먼저 이 글을 포스팅하게 된 계기부터 간략히 말해보자면, 오픈소스 컨트리뷰션 아카데미의 ArgoCD 멘티로 참여하게 되었다.

회사에서 ArgoCD를 사용해서 호기롭게 '도전'했지만, 기본기가 퐁당퐁당 돌을 던진 급으로 빠져있다는 걸 알았다.

그래서 멘토님께서 추천해주신 Udemy의 docker, k8s의 강의를 들어보기로 했다. 

https://www.udemy.com/share/106b8i3@vCPASiYpcAc4ojgaRCE817Cz6JhrhOL5yFiwXv6N6E1gTfjcpQ14ACV1W47MGnqHqQ==/

 

할인이라길래 헐레벌떡 들어가서 결제하려고 했는데,, 2022년에 이미 결제했다고 한다 ㅎ,ㅎ,,,

사실 도커를 어느 정도 다룰 줄은 안다. 개념학습을 하지 않고, 일단 무작정 동작하는 것에 의의를 두고 해본 적은 있다.

2022년에 강의를 결제했다가 무조건 실습에 부딪히느라 보질 않았었던 것 같다.

 

이번 포스팅을 통해 나에 대한 이해는 물론, 읽는 사람도 잘 이해가 되었으면 좋겠다. 

일단 강의를 보고나서 느낀 점은 눈 앞의 왕도를 두고, 2년 걸리는 가시밭길로 삥 둘러 왔다는 생각이 든다.

일단 제일 처음 포스팅은 도커의 개념에 대해 어느 정도 알고 있다는 가정하에 이미지와 컨테이너 개념부터 시작하려고 한다.

 


도커의 이미지란?

이미지는 컨테이너에 필요한 모든 코드와 명령을 담은 패키지이다.

이미지는 다른 사람이 만들었거나, 공식으로 만들어진 이미지가 있다.

물론, 내가 이미지를 직접 생성할 수도 있다. 그리고 Docker Hub라는 곳에 올려주면 다른 사람이 내 이미지를 사용할 수도 있다!

 

도커의 컨테이너란?

컨테이너는 실행 중인 이미지 인스턴스이다. 이미지를 실행하면 그게 바로 컨테이너가 되는 것이다.

 

컨테이너와 이미지의 핵심 키워드는 isolation(격리)와 read only(읽기 전용)라고 생각한다.

 


 

 

강의에서는 Node.js 애플리케이션으로 예제를 들고 있는데, 나는 이전에 Go 서버를 구축한다고 만든 간단한 서버 코드를 활용하였다.

화면 없어도 response json 데이터만 봐도 괜찮다고 생각하기 때문이다(절대 귀찮아서가 아니다..ㅎ.ㅎ).

 

아래는 간단한 Go 서버를 띄우는 Dockerfile이다. 이 Dockerfile을 빌드해서 이미지를 생성하는 것이다.

그렇다면 각각의 명령어에 대해 설명해보려고 한다. 

FROM golang

WORKDIR ./app

COPY . .

RUN go mod tidy

WORKDIR ./init

EXPOSE 8080

CMD ["go", "run", "main.go"]

FROM

일반적으로 Dockerfile은 FROM으로 시작한다.

가장 베이스, 기초가 되는 이미지를 구축할 수 있다. 

기초가 되는 이미지는 주로 실행하려는 코드의 언어 이미지가 되지 않을까 싶다.

어떤 이름을 넣어야 하는지 모르겠다면 Docker Hub에 가서 내가 사용한 언어를 검색해보자!

 

COPY

도커를 우리의 로컬 머신(컴퓨터)에서 실행하는 거긴 하지만, 이미지 내부에 로컬 머신과 완전 분리된 자체 내부의 파일 시스템이 있다.

그러므로 로컬 머시의 어떤 파일들이 이미지 내부 파일 시스템으로 들어가야 하는지 알려줘야 한다.

'COPY . .'를 예시로 들어 설명해보자면, 첫번째 인자는 로컬 머신의 파일 경로이며, 이미지로 복사되어야 할 파일들이 있는 곳이다.

Dockerfile은 항상 루트 디렉토리의 바로 하위에 존재해야 하며, 루트 디렉토리를 기준으로 파일 경로를 선언한다.

두번째 인자는 이미지 내부 파일 시스템에서 어디로 넣을지 경로를 입력하면 된다.

 

WORKDIR

어느 경로에서 명령을 진행해야 하는지 정해준다.

WORKDIR 명령 이후의 명령어는 해당 경로에서 명령어를 실행하게 되는 것이다.

 

RUN

RUN 명령을 터미널에서 그대로 쳤을 때 실행하는 것이라 생각하면 된다.

RUN go mod tidy는 내가 Go 프로젝트 터미널에서 의존성을 설치할 때 치는 명령어이다.

이걸 격리된 이미지 내부에서 명령어를 실행하게끔 해놓은 것이다.

 

EXPOSE

내 컴퓨터(로컬머신)에서 8080포트로 프로젝트를 실행하고, localhost:8080을 검색하면 정의한대로 뜨는 것이 정상 동작이다.

그러나 내 코드들을 도커의 이미지로 만들게 된다면 추가로 어떤 포트로 실행할 것인지 정의해줘야 한다.

이미지가 실행하고 있는 도커 컨테이너에서는 로컬과 격리되어 있다. 즉 자체의 내부 네트워크가 있다.

컨테이너 내부 애플리케이션에서 포트 8080을 받을 때 컨테이너는 그 포트를 우리의 로컬 머신에 노출하지 않는다. 

언제나 Dockerfile의 마지막 명령 전에 로컬 시스템의 특정 포트에다가 노출하고 싶다는걸 알려줘야 한다.

 

CMD

go언어 베이스 이미지를 구축하고, 소스 코드를 모두 내부 파일 시스템으로 복사하고 의존성을 설치하고 포트까지 노출하면 이제 프로젝트를 실행해보고 싶다. 그러나 이미지는 실행하는 것이 아니라는 걸 명심하자. 이미지를 기반으로 컨테이너를 실행시켜야 한다.

이미지를 빌드할 때마다 실행하는 것은 올바르지 않다고 한다.

아까 이미지 내부 파일 시스템에서 RUN go mod tidy를 하여 바로 실행한 것과 다르게 여기서 CMD와 RUN의 차이를 알게 된다.

RUN은 바로 실행하는 것이다. 그러나 CMD는 이미지를 빌드하고, 빌드된 이미지로 컨테이너가 시작할 때 실행되는 명령어를 넣어줘야 한다. 


 

 

위의 명령어를 기반으로 Dockerfile을 모두 작성했다면, 이제 이미지를 빌드해야 한다.

docker build . 하면 이미지가 빌드될 것이다.

이미지가 성공적으로 빌드된다면 마지막에 생성된 이미지 id를 보여준다.

 

그럼 이제 빌드한 이미지를 기반으로 실행하는 컨테이너를 만들어보자!!

docker run {생성된 이미지 id}

중괄호는 안넣어도 된다.(난 중괄호가 있으면 넣어야 하는지 항상 헷갈렸었다 ㅎ)

그럼 이제 localhost:8080으로 접속해보면 된다. (포트는 자기가 원하는대로 해봐도 된다~!!)

이전에 이미지를 빌드할 때 우리가 8080으로 EXPOSE할 거라고 정의했기 때문에 localhost:8080으로 시도하는 것이다.

 

그러면 아마 접속이 안될 것이다. 단순히 'docker run 이미지'를 실행하면 안된다.

이제 docker run 명령어에 p를 붙여서 다시 시도해볼 것이다. 여기서 p는 publish를 뜻한다.

우리의 로컬 머신의 어떤 포트(첫번째 포트)가 도커 내부의 특정 포트(두번째 포트)에 액세스할 수 있도록 publish 하겠다는 뜻으로 아래의 명령어를 실행할 것이다.

docker run -p 8080:8080 {생성된 이미지 id}

그런 다음 다시 localhost:8080으로 접속하면 정상적인 접속이 되는걸 확인할 수 있을 것이다.

 


 

도커의 특성(정리)

내 소스 코드를 컨테이너 내부에서 관리하는 파일 시스템에 복사하도록 한다.

의존성 설치하고, 컨테이너 시작될 때 서버 시작하라는 명령까지 내린다.

 

소스 코드를 내부 파일 시스템에 복사한 이후에 로컬에서 코드를 변경한다면 다시 새로운 이미지를 빌드하기 전까지 변경이 반영되지 않는다. 이미지는 읽기 전용이며, 한번 빌드되면 끝이다. 외부에서 편집할 수 없는 개념이다. 

그래서 소스코드를 변경하면 기존의 이미지를 실행하는 컨테이너는 변경이 반영되지 않는다. 예전 사진을 가지고 실행하고 있는 것이기 때문이다.

변경된 부분을 확인하려면, 소스코드를 변경해주고 다시 이미지를 빌드하면 된다. 그럼 변경이 일어날 때마다 매번 베이스 이미지를 pull 해오고, 복사하고, 의존성 설치하고.. 이런 과정을 처음부터 반복하는게 과연 효율적일까? 즉, 소스코드를 변경할 때마다 이미지를 빌드하는게 효율적일까? 

 

이 문제점을 해결하기 위한 특성이 바로 Docker의 레이어 기반 아키텍처이다.

Dockerfile을 빌드하는 과정에서 명령어의 결과가 이전과 동일하다고 판단되면, 이전의 캐시 결과를 사용한다.(Using Cache)

즉 Dockerfile의 명령어 한 줄 하나하나가 레이어고, 레이어마다 이전과 동일하면 캐시 결과를 사용하는 것이다.

이는 이미지 생성 속도를 높이고자 나타난 개념이다.

만약 레이어가 다르다고 판단되면, 그 이후의 레이어부터는 캐싱된 결과를 사용하지 않고 다시 빌드한다.

 

이 특성을 이용하여 작은 최적화를 진행해볼 수 있다. 레이어의 위치를 조정하는 것(=명령 순서를 재배치하는 것)도 최적화 과정 중 하나가 될 수 있다!

그럼 위의 Dockerfile 레이어 중 COPY와 RUN을 다시 분석해보자. 

 

RUN의 경우에는 의존성을 설치하는 단계이고, COPY는 소스 코드를 내부 파일 시스템에 복사하는 단계이다.

의존성이 변경되는 것보다는 소스 코드가 빈번히 일어나는 상황이라고 가정하자

(이미 필요한 라이브러리는 모두 받은 상태이고, 개발만 하는 경우가 주로 이런 상황이 아니지 않을까 싶다!)

 

위의 Dockerfile대로 한다면 COPY를 하면서 변경이 감지되고, 다시 처음부터 복사를 진행하고 처음부터 의존성 설치 작업이 발생한다.

그러나 COPY와 RUN의 순서를 바꾼다면 의존성이 이전과 동일하다면 지나치고, 복사하는 경우에만 다시 작업을 진행할 것이다.

이렇게 도커의 특성을 알아두면 도커파일도 최적화를 진행할 수 있구나,, 라는 걸 깨달은 학습의 시간이었다!

 

ent Schema를 생성한 후, Field의 docs를 읽으면서 어떻게 속성을 정의해주는지 확인하려고 했습니다.

Field를 클릭했는데, Cannot find declaration to go to 라고 뜨면서 import가 되지 않았다는 걸 깨달았습니다ㅜ

go.mod를 가니 unresolved dependency 80개가 우르르 떴습니다.

 

mac 기준 일반적인 해결방안은 Settings > Go > Go Modules > Enabling Go modules intergration 하는 것이었습니다. 

추가로 Go Path > deselect index entire GOPATH 인 것도 있었습니다.

 

이 모든 것들이 기본적으로 되어 있는 상황이었어서, 또 다른 해결방안을 찾아봐야 했습니다.

제 프로젝트에서는 .idea 디렉토리가 존재했습니다. 여기에는 .gitignore 파일, 등등 다양한 파일이 있었습니다.

.gitignore 파일은 루트로 이동시키고, .idea 디렉토리를 삭제하였습니다. (다른 파일은 사용하지 않는 파일이라 판단)

그리고 다시 해당 프로젝트를 열었더니 의존성을 처음부터 설치하면서 unresolved dependency 문제를 해결할 수 있었습니다.

 

문제 상황

Upstage의 solar LLM API를 연결하여 AI 챗 기능을 구현하려고 했습니다. Feign client로는 카카오 로그인과 개발 중인 서비스의 AI 모델들을 이용하여 호출하도록 구현해놓은 상황이었습니다.

그동안 Feign을 잘 몰랐어도 외부 API를 호출하기 쉬웠고, 이번 LLM 모델 API도 Feign을 이용하여서 구현할 계획이었습니다.

API 호출에 필요한 설정은 물론 응답 정보를 담는 dto, 호출할 수 있도록 하는 controller를 모두 만들어놓고 포스트맨으로 실행했는데, 예상과 달리 아래와 같은 로그가 남게 되는 것이었습니다.

 

원인 분석 과정

1. 실제 API 응답을 받는 dto 멤버변수명 확인 필요

Unknown Source...?? '외부 API에서 응답을 받아와서 직렬화할 때 문제가 발생하는 것인가?'하고 dto 변수명들을 꼼꼼히 훑어봤습니다. 변수명이 틀린 부분은 없었습니다.

 

2. API 호출 설정에 대한 의심 필요 -> 직접 curl 요청

직접 터미널에서 curl 요청을 해도 정상적인 응답을 받았습니다.

 

3. Bean 주입이 제대로 되었는지 확인 필요

당시에 Feign을 활용한 다른 서비스와 동일하게 설정했음에도 안됐어서, 이 부분에 대해서는 체크하지 못했습니다.

 

해결 방안

getChatResult 메서드는 feign client에 선언된 메서드를 호출합니다. 그리고 필요한 데이터만 정의한 dto에 맞게 다시 response를 가공해서 controller단에 반환해주는 역할을 합니다. 근데 저기서 Unknown Source라고 에러가 나타나니, 빈이 제대로 주입되지 않았나?하는 의문이 생겼습니다. 그러나 동일하게 만들어진 다른 곳에서는 정상 동작을 하고 있었기 때문에 원인을 찾기가 힘들었습니다(이 때 많은 현타가 왔습니다..ㅜ 제대로 알고 활용한 게 아니라는 반성이 들었습니다..ㅜ)

 

당시 대회 과제를 빠르게 제출해야 했기 때문에, 원인 분석에 지체할 시간이 없었습니다. Feign은 동적 프록시를 사용하여 API 호출을 간단하게 해줍니다. 당시에 Feign client를 찾을 수 없다고 했기 때문에, 프록시에 의존하는 것이 아니라 직접 http 클라이언트를 만들어야 했습니다. 그래서 okhttp 라이브러리를 활용했고, 정상적인 응답을 받을 수 있었습니다.

배경

이번 JUNCTION ASIA 2024에 참여하게 되었습니다. 해커톤에서 어떤 챌린지를 해보는게 좋을까 고민하였습니다.

처음에는 높은 완성도를 위해서 개인적인 템플릿이 많이 갖춰져있는 Java로 구현하려고 했습니다. 그러나 실무에서 사용하는 Golang으로 서버를 구축하고 서비스를 만들어보고 싶다는 욕심이 계속 생겨서, Go로 서버를 구축하기로 결정했습니다.

 

 

해커톤 전까지 인프런에서 Go 서버 구축 관련한 기초 강의를 들으며, 어떤 라이브러리를 쓸지에 대해 미리 생각해봤습니다.

아래 링크의 강의를 학습하며 미리 구축하고자 했습니다.

그러나 모든 것들을 강의에 맞춰 진행하지는 않았습니다. 강의에서는 DB 연동을 하지 않고 있기 때문에, 실제 개발을 하는데에 크게 도움이 되지는 않습니다. 프로젝트 골격을 만드는 데에는 많은 도움을 줬습니다. 

그래서 프레임워크/ORM/의존성 주입 관리를 할 때 어떤 기술을 사용할 지에 대해 고민하였습니다.

https://www.inflearn.com/course/golang-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EB%B0%8F%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95

 

Golang을 통한 백엔드 개발 및 환경 구축하기 강의 | July - 인프런

July | Golang을 통해서 CRUD를 어떻게 구성하는지는 물론, 프론트엔드와 협업할 때 사용 가능한 PostMan 활용법과 Repository 관리 및 환경 구축까지! 실제 실무에서 필요한 지식을 담았습니다., [사진]Gola

www.inflearn.com

 

 

선택한 기술들(gin + Ent + fx)

프레임워크는 gin으로 사용할 것입니다.

레퍼런스가 작은 Go 특성을 생각하여, 그나마 레퍼런스를 빠르게 찾을 수 있도록 대중적인 프레임워크를 선택했습니다.

 

ORM 기술로는 ent를 사용할 것입니다.

실무에서 GORM을 사용하면서 ORM이라기보다는 직접 쿼리를 작성해야 하는 경우가 많아 JPA와 비교했을 때 불편함을 느꼈습니다. 특히, RecordNotFound() 메서드의 경우, 데이터가 있을 때 false를 반환하는데, 메서드 이름과 동작이 직관적이지 않아 불만이 있었습니다. JPA를 사용하다가 GORM으로 넘어오니 직접 쿼리를 작성해야 하는 상황이 자주 발생해 ORM의 편리함을 충분히 느끼지 못했습니다. 그래서 이번에는 Ent를 사용해보았습니다. Ent는 다양한 메서드를 제공하며, 코드 가독성이 뛰어나 선택하게 되었습니다.

 

의존성 주입 관리 기술로는 uber의 fx를 사용할 것입니다.

처음에는 이게 왜 필요하지?라는 생각이 들었는데, 위의 강의를 따라치며 만든 코드와 [블로그](https://www.essential2189.dev/go-fiber-boilerplate) 글을 비교해보며, 의존성 주입 관리 기술이 필요하다는 생각이 들었습니다.

블로그의 본문 내용에 따라 fx를 선택하기로 했습니다.

 

 

목표

Go 코드나 Go 프로젝트가 Java처럼 보이지 않도록 Java에서 벗어난 프로젝트처럼 보이고 싶었습니다.

갓 취업을 했을 때 저는 Java 신봉자였습니다. Java만큼 객체 지향에 대한 레퍼런스가 많고, 객체지향이 잘 되도록 구성된 언어는 없다고 생각했고, 다른 언어는 이런 점을 따라가야 한다고 생각했습니다.

그래서 회사 코드를 볼 때 'Java였다면..'라는 생각을 놓을 수가 없었습니다. 어떤 부분이든 Java 기준을 잣대로 코드를 리팩토링하려고 했습니다. 업무를 하면서 느낀 점은 언어마다의 특성을 이해하고 그 언어의 특징을 살려줘야 한다는 생각이 들었습니다. 

이번 사이드 프로젝트를 Go로 진행하면서 목표를 달성하고자 했습니다. 나아가서 Go를 이용한 프로젝트 템플릿이 없다는 걸 많이 깨달아 이번 기회에 Go로 프로젝트를 쉽게 시작할 수 있는 템플릿을 만들어보고자 했습니다.

https://www.acmicpc.net/problem/2563

어려운 문제 풀이는 스스로 풀이하기 어렵다는 생각이 들어서, 일단 실버 5단계 문제부터 다시 차근차근 풀어보자!!

 

문제 제한 사항

시간 효율을 고려하지 않아도 될 정도로 넉넉한 문제인 것 같음.

 

문제 해결 아이디어

- 가로, 세로의 길이가 100인 흰색 도화지와 가로, 세로의 길이가 10인 검은색 색종이가 있다.

- 입력값은 다음과 같다.

색종이의 왼쪽 변과 도화지의 왼쪽 변 사이의 거리, 색종이의 아래쪽 변과 도화지의 아래쪽 변 사이의 거리

- 첫번째 숫자의 오름차순으로 정렬이 필요하다.

- 숫자값들을 비교해가면서 값을 빼나가야 한다고 생각했다. => 수학적인 접근이 필요한 문제라고 생각했다.

- 근데 2차원 배열을 어떻게 활용해야 할까?라는 고민이 들면서 확어려워짐..

 

- 흰색 도화지만큼의 가로, 세로 길이가 100인 2차원 배열을 만든다.

- java는 2차원 배열을 초기화할 때, 별다른 값을 fill하지 않는 이상 false를 채워넣는다.

- 아직 아무런 색종이가 침범하지 않은 영역이라면 true로 변경하고, 넓이값++을 해준다.
여기서 처음엔 겹치는 부분을 구해서 빼줘야 겠다고 생각했는데 그냥 겹치지 않는 부분을 하나씩 더해주는 것도 하나의 방법이라는 걸 깨달았다.

- 만약 다른 색종이가 침범한 영역이라면 이미 true이기 때문에 더해지지 않고, 다음으로 넘어간다.

=> 최종 total의 값이 침범하지 않은 영역의 총합이 된다.

 

코드

public class Problem01 {
    public static void main(String[] args) throws IOException {
        Problem01 problem = new Problem01();

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int total = 0;
        int n = Integer.parseInt(br.readLine());
        boolean[][] result = new boolean[101][101];
        boolean[][] mock = new boolean[3][3];

        for (int i=0; i<n; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            int x = Integer.parseInt(st.nextToken());
            int y = Integer.parseInt(st.nextToken());

            for (int j=x; j<x+10; j++) {
                for (int k=y; k<y+10; k++) {
                    if (!result[j][k]) {
                        result[j][k] = true;
                        total++;
                    }
                }
            }
        }
        System.out.println(total);
    }
}

배운점

'알고리즘' 카테고리의 다른 글

프로그래머스 - 거리두기 확인하기  (2) 2024.06.11
프로그래머스 - 삼각 달팽이  (1) 2024.06.09
프로그래머스 - 교점에 별 만들기  (2) 2024.06.08
비트마스크(1)  (0) 2023.03.14
그래프와 인접행렬  (0) 2022.12.11

문제 제한 사항

문제 해결 아이디어

맨하탄 거리 구하는 공식

(x1, y1)과 (x2, y2)가 있을 때 |x1-x2| + |y1-y2| 가 맨하탄 거리이다.

 

거리두기를 올바르게 지킨 경우는 두가지로 분리할 수 있다.

1. 맨하탄 거리가 3 이상일 경우

2. 맨하탄 거리가 2이지만, 파티션으로 막혀있는 경우

 

거리두기를 올바르게 지키지 않은 경우는

1. 맨하탄 거리가 1일 때

2. 맨하탄 거리가 2이고, 하나라도 빈 책상이 사이에 있는 경우

 

대기실별로 거리두기를 모두 지키고 있다면 1을, 한 명이라도 지키고 있지 않다면 0을 반환해야 한다.

 

존재하면 안되는 곳에 있는지만 체크하면 된다. 

 

안되는 예외 사항을 빠르게 발견해서 return 처리하면 답을 빠르게 구할 수 있을 줄 알았다.

근데 예외 사항이 생각보다 많았다. 일반적인 경우, 즉 일반식을 도출해서 구해야한다는 걸 깨달았다.

 

코드

input과 같은 2차원 배열을 또 다른 하나의 2차원 배열로 나눌때 사용되는 코드 템플릿

String[][] input = {
    {"POOOP", "OXXOX", "OPXPX", "OOXOX", "POXXP"},
    {"POOPX", "OXPXP", "PXXXO", "OXXXO", "OOOPP"},
    {"PXOPX", "OXOXP", "OXPOX", "OXXOP", "PXPOX"},
    {"OOOXX", "XOOOX", "OOOXX", "OXOOX", "OOOOO"},
    {"PXPXP", "XPXPX", "PXPXP", "XPXPX", "PXPXP"}
 };


// 위와 같은 2차원 배열인데, 또 저기서 나눠줘야 할 때
for (int i=0; i<input.length; i++) {
    String[] place = input[i];
    char[][] room = new char[place.length][];
    for (int j=0; j<room.length; j++) {
        room[j] = place[j].toCharArray();
    }
 }

 

배운점

너무 오랜만에 코딩테스트 문제를 푸니깐 생각하기가 귀찮아진다.ㅜ,, 아직까지 다시 어떻게 시작해야하는지 감이 안와서 고민하는 시간을 가지고 결국엔 정답 보는 흐름대로만 풀고 있는데, 계속해서 도전은 해봐야할 것 같다.

'알고리즘' 카테고리의 다른 글

백준 - 색종이  (1) 2024.06.13
프로그래머스 - 삼각 달팽이  (1) 2024.06.09
프로그래머스 - 교점에 별 만들기  (2) 2024.06.08
비트마스크(1)  (0) 2023.03.14
그래프와 인접행렬  (0) 2022.12.11

링크

https://school.programmers.co.kr/learn/courses/30/lessons/68645

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

문제 제한 사항

정수 n이 매개변수로 주어지고, 최소 1 이상이고 최대10^3 이다.

최대 시간 복잡도는 O(N^2)이다. 

 

문제 해결 아이디어

n=4 이고, 블록에 들어가는 숫자를 e라고 지칭할 때

e=1 , n=1

e=2, n=2

e=3, n=3

e=4, n=4

e=5, n=4

e=6, n=4

e=7, n=4

e=8, n=3

e=9, n=2

e=10, n=3

위와 같이 들어가게 된다.

 

처음에는 n값과 e값이 동일하게 증가하다가,

마지막 층인 n이 4일 때, 4개의 블럭이 다 찰 때까지 연속으로 숫자를 넣어준다.

그리고 e값은 증가하면서 다시 블럭이 다 안 찬 층을 찾으면 숫자를 바로 넣어주는 형식이 된다.

 

멤버변수로 자신의 n값과 어떤 값이 들어가는지 Integer 리스트를 멤버변수로 가질 수 있도록 하려고 했다.

 private static class Level {
    public int n;
    public List<Integer> elements;

    public Level(int n) {
        this.n = n;
        this.elements = new ArrayList<>();
    }
}

 

연속된 숫자와 맨마지막 구간에서 차례대로 숫자를 넣는 부분까지는 구현하기 쉬웠으나, 그 이후 순서부터 조금씩 꼬이기 시작했다.

 

코드

> 기존 코드(오답) .. 너무 별로인 코드ㅜ

조건 처리하는 데 예외 사항에 대한 처리가 명확하지 않았고,

'층 수가 줄어드는 경우 / 층 수가 증가하는 경우 / 층 수가 유지되는 경우'에 대한 조건을 마구잡이 형식으로 넣어줘서 정답이 제대로 나오지 않았다.

논리 단위로 동작 흐름을 나누는 부분에 대해 부족하게 느껴졌다.

    private static class Level {
        public int n;
        public List<Integer> elements;
        public boolean canPutElement;

        public Level(int n) {
            this.n = n;
            this.canPutElement = true;
            this.elements = new ArrayList<>();
        }

        private void addAllElements(int elementNum) {
            int number = elementNum;
            while (elementNum <= number + (number-1)) {
                elements.add(elementNum++);
            }
            this.canPutElement = false;
        }

        private void addElement(int elementNum) {
            if (canPutElement) {
                elements.add(elementNum);
                if (elements.size() == n) {
                    canPutElement = false;
                }
            }
        }
    }


public static void main(String[] args) {
//        int n = 4;
        int n = 5;
//        int n = 6;

        Map<Integer, Level> levelMap = new HashMap<>();

        for (int i=1; i<=n; i++) {
            Level level = new Level(i);

            if (i==n) {
                level.addAllElements(i);
            } else {
                level.addElement(i);
            }

            levelMap.put(i, level);
        }

        int flag = n-1;
        for (int element=2*n; ; element++) {
            Level level = levelMap.get(flag);
            if (flag == n && !level.canPutElement){
                break;
            }

            if (flag == n) {
                level.addElement(element);
                continue;
            }

            if (level.canPutElement) {
                level.addElement(element);
                flag--;
            } else {
                flag++;
                level = levelMap.get(flag);
                level.addElement(element);
            }
        }

        for (Level level : levelMap.values()) {
            System.out.println(Arrays.toString(level.elements.toArray()));
        }
    }

 

> 정답은 다른 블로그에 찾아보면 많이 나오기 때문에 따로 첨부하지는 않으려고 한다.

배운점

시작한지 얼마 되지 않아서, 일단 지금 형식처럼 풀어보고 기록하려고 한다. 

계속 연습해서 코딩테스트 준비에 차질이 없도록 해야겠다.

'알고리즘' 카테고리의 다른 글

백준 - 색종이  (1) 2024.06.13
프로그래머스 - 거리두기 확인하기  (2) 2024.06.11
프로그래머스 - 교점에 별 만들기  (2) 2024.06.08
비트마스크(1)  (0) 2023.03.14
그래프와 인접행렬  (0) 2022.12.11

링크

https://school.programmers.co.kr/learn/courses/30/lessons/87377

 

문제 해결 방식

[제한 사항]

2 <= line의 행 길이 <= 10^3  ➡ 시간 복잡도는 O(n^2) 이하여야 한다.

line의 열 길이 = 3 

행의 형식은 (A, B, C) => Ax + By + C = 0

행의 원소는 정수이며, -10^4 이상 10^4 이하

별이 한 개 이상 그려지는 입력만 주어진다.

 

문제 해결 아이디어

Ax + By + C = 0

Dx + Ey + F = 0 이 있다고 가정할 때,

1. -B/A와 -D/E가 다르다면 기울기가 다르기 때문에 교점이 한 개 발생할 수 있다.

2. 교점 좌표를 구하는 공식은 x = (BF-EC)/(AE-DB), y = (DC-AF)/(AE-DB) 이다.

3. 정수 좌표라면 저장하고, 아니라면 건너뛴다.

4. 저장된 좌표 중에서 x의 최댓값과 y의 최댓값을 저장해야 한다.

 

코드

교점을 구하는 과정

ArrayList<Point> points = new ArrayList<>();
    for (int i=0; i< lines.length; i++) {
	    for (int j=i+1; j< lines.length; j++) {
			Point intersection = intersection(lines[i][0], lines[i][1], lines[i][2],
                                                lines[j][0], lines[j][1], lines[j][2]);
			if (intersection != null) {
     	       points.add(intersection);
            }
        }
    }
private Point intersection(long a1, long b1, long c1, long a2, long b2, long c2) {
    double x = (double) (b1 * c2 - b2 * c1) / (a1 * b2 - a2 * b1);
    double y = (double) (a2 * c1 - a1 * c2) / (a1 * b2 - a2 * b1);

    if (x % 1 != 0 || y % 1 != 0) {
        return null;
    }
    return new Point((long)x, (long)y);
}

 

 

별점을 찍는 코드

Point minimum = getMinimumPoint(points);
Point maximum = getMaximumPoint(points);

int width = (int) (maximum.x - minimum.x + 1);
int height = (int) (maximum.y - minimum.y + 1);

char[][] arr = new char[height][width];
for (char[] row : arr) {
    Arrays.fill(row, '.');
}

for (Point p : points) {
   int x = (int) (p.x - minimum.x);
   int y = (int) (maximum.y - p.y);
   arr[y][x] = '*';
}

String[] result = new String[arr.length];
for (int i=0; i<result.length; i++) {
    result[i] = new String(arr[i]);
}
return result;

 

배운 점

너무 오랜만에 하는 거라, 구현 능력이 많이 떨어진다는걸 알게 되었다.

기울기가 다른지 굳이 더블체크할 필요가 없었다. 교점만 체크하면 되기 때문에 궁극적으로 구해야하는 부분, 구해야 하는 값을 위해 최소한으로 계산을 해야하는 부분만 체크해야 한다.

미리 코드의 템플릿을 만들어둬야겠다.