소개
- (현) 카카오 백엔드 엔지니어
- (수상) 🏆 공개 SW 개발자 대회 [2020 일반부문 / 금상_정보통신산업진흥원장상]
현재 카카오에서 일하고 있고, 만드는 것을 좋아해서, 퇴근 후에도 항상 무언가를 개발하고 있습니다.
"거인의 어깨 위에 선 난쟁이"라는 말이 있습니다. 저 역시 한낱 작은 난쟁이일 뿐이지만, 올라탄 거인의 성장에 도움이 될 수 있도록 지식의 대물림을 위해 노력하고 있습니다. 다수의 주니어 개발자분들을 멘토링 한 경험이 있어서 여러분의 성장을 도와줄 수 있을 거예요.
깃허브 > https://github.com/kok202
블로그 > https://kok202.tistory.com
강의
수강평
- Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트
- Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트
- Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트
- Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트
게시글
질문&답변
혹시 git 플러그인 아시는 분
안녕하세요. 어떤 기능을 물어보시는지 정확히 잘 이해가 안 되는데요. 혹시 아래 기능을 말하시는 게 맞으실까요?(사진)이는 IntelliJ 기본 내장 기능입니다. 커밋 앞 > Run SpotBugs analysis, 커밋 앞 > Scan with Checkstyle 정도만 IntelliJ 추가 플러그인을 설치해서 표기되는 내용이고요. (각각 SpotBugs, Checkstyle 플러그인입니다.)혹시 IntelliJ Ultimate 전용 기능인가 싶어 Community에서는 안보이나도 확인해 봤습니다. 그리고 Ultimate, Community 관계없이 잘 보이는 것도 확인했습니다.IntelliJ에서 Commit 방식을 탭 버전으로 하고 있는 것은 아닌가요? modal 버전을 사용하도록 설정 변경해 보는 것은 어떤지요? (여기: https://engineerinsight.tistory.com/90) 참고하면 좋을 것 같습니다.번외 참고: https://intellij-support.jetbrains.com/hc/en-us/community/posts/360007373539-The-new-Commit-tab
- 0
- 1
- 371
질문&답변
패키지 의존성을 확인해보는법?
사용해 본 적이 없기에 툴의 존재는 잘 모르겠네요. 알고 있었다면 저도 강의에서 소개했을 것 같습니다.😭 저도 매번 찾아볼 때마다 원하는 수준으로 나오는 걸 못 봐서, 그냥 일일이 눈으로 확인하고 있었습니다.그래서 질문 주셨기에 마침 기회다 싶어서 다시 찾아봤는데요. 그나마 가장 괜찮은 방법은 IntelliJ Ultimate 버전의 분석 기능이 아닌가 싶습니다. 다음과 같은 분석 기능을 제공해 주네요.(사진)각 기능 모두 패키지, 클래스 수준까지 의존성을 확인할 수 있습니다.참고.1 https://jaehoney.tistory.com/311참고.2 https://v0o0v.tistory.com/4그런데 저의 니즈도 다이어그램으로 보고 싶은 것이라, 솔직히 위 기능이 얼마나 쓸모 있는지는 잘 모르겠습니다.
- 0
- 2
- 1.2K
질문&답변
도메인 객체와 영속성 객체를 구분하게 되면
안녕하세요. 근래에 책을 집필할 기회가 생겨 그쪽에 힘을 실어주다 보니 다른 일에 신경 쓰지 못했습니다. 답변이 늦어 죄송합니다. 다만 해당 강의는 공식적으로 질의응답을 제공하지 않는 강의였다는 점을 이유로 늦어진 부분에 대해 양해 부탁드립니다.김남호 님이 좋은 답변해 주신 것 같네요. 감사합니다.이야기하신 대로 도메인 객체와 영속성 객체를 구분하면 JPA에서 제공해 주는 기능들을 사용하지 못합니다. 그런데 이것이 헥사고날 아키텍처와 의존성 역전 원칙이 추구하는 방향입니다. 두 이론 모두 시스템이 특정 라이브러리에 종속되지 않으려면 어떻게 해야 하는지를 이야기하고 있습니다.이와 관련해서는 이전에 비슷한 답변을 한 적이 있기에 링크로 대체하겠습니다.https://www.inflearn.com/questions/947209/jpa의-더티체킹-사용에-대해서https://www.inflearn.com/questions/945855/comment/280701https://www.inflearn.com/questions/978220더불어 양방향 매핑은 사실상 순환 패턴으로 안티 패턴입니다. 근본적으론 양방향 매핑을 안 만드는 것이 제일 좋습니다. 단방향 매핑으로만 객체 관계를 정의하면 생각보다 생성 수정이 쉽습니다.
- 0
- 3
- 1.4K
질문&답변
UUID, Random 등 자주 사용하는 의존성의 경우
안녕하세요. 근래에 책을 집필할 기회가 생겨 그쪽에 힘을 실어주다 보니 다른 일에 신경 쓰지 못했습니다. 답변이 늦어 죄송합니다. 다만 해당 강의는 공식적으로 질의응답을 제공하지 않는 강의였다는 점을 이유로 늦어진 부분에 대해 양해 부탁드립니다.공통으로 사용하는 유틸 클래스들을 하나의 패키지에 모아 관리하는지가 궁금한 것으로 보이는데 맞으실지요? 네, 그렇게 사용합니다. util 패키지가 생기는 것 자체는 전혀 이상한 게 아닙니다. 스프링 프레임워크에서도 util 패키지 사용합니다. (참조: https://github.com/spring-projects/spring-framework/tree/main/spring-core/src/main/java/org/springframework/util) 그리고 ClockHolder, UuidHolder는 충분히 util 패키지에서 관리할 만한 인터페이스라 생각합니다.다만 그렇다고 모든 프로젝트에서 항상 util 패키지를 만들어 사용하는 것은 아닙니다. 이는 상황에 따라 다릅니다. util 패키지를 만드는 경우, 도메인으로 엮여야 할 비즈니스 로직들이 종종 클래스 이름에 util이라는 이름을 달고 util 패키지로 모이는 경우가 생깁니다. 그래서 프로젝트 별로 util 패키지 자체를 만드는 것을 선호하지 않기도 합니다.그러니 ClockHolder, UuidHolder의 위치는 common.domain에 들어가도 되고 util에 들어가도 된다 생각합니다. 사실 클래스의 패키지 위치나 이름 자체는 그렇게 중요한 문제가 아닙니다.원하시는 답변이 됐는지 잘 모르겠습니다. 답변이 도움 됐길 바랍니다. 감사합니다.+) 음… 그런데 제 생각엔 본질적으로 이 질문은 실제로 ‘어떻게 사용하는지가 궁금하다’기보다, 개발에 대한 확신이 없어서 긴가민가하고 있는 것에 기인한 게 아닐까 싶습니다. 그래서 답을 원하는 것 같기도 하고요…? (어차피 개발에 정답은 정해져 있지 않습니다. 확신과 근거만 있으면 됩니다. 만들어 보고, 막상 만들어 봤더니 별로다 싶으면 수정하면 되죠.) 그러니 그냥 마음내키는 대로 개발해 보는 게 좋지 않을까 싶네요!
- 0
- 2
- 636
질문&답변
n+1질문입니다!
안녕하세요. 근래에 책을 집필할 기회가 생겨 그쪽에 힘을 실어주다 보니 다른 일에 신경 쓰지 못했습니다. 답변이 늦어 죄송합니다. 다만 해당 강의는 공식적으로 질의응답을 제공하지 않는 강의였다는 점을 이유로 늦어진 부분에 대해 양해 부탁드립니다.Member ↔ Team 관계에서 팀과 팀원을 나타내는 정보가 아래와 같다고 가정합시다.@Getter @Builder class Team { private long id; private String name; } @Getter @Builder class Member { private long id; private String name; private Team myTeam; } @Data @NoArgsConstructor @Entity(name = "team") class TeamJpaEntity { @Id private String id; @Column private String name; public Team toModel() { ... } } @Data @NoArgsConstructor @Entity(name = "member") class MemberJpaEntity { @Id private String id; @Column private String name; @ManyToOne @JoinColumn(name = "my_team_id") private TeamJpaEntity myTeam; public Member toModel() { ... } } 이때 팀 정보와 팀원 정보를 같이 있는 도메인 객체인 TeamWithMembers 라는 도메인이 있다 가정합시다. TeamWithMembers은 다음과 같습니다.class TeamWithMembers { private long id; private String name; private List members; } 그리고 TeamWtihMembers의 JpaEntity는 없습니다. 그러면 이때 TeamWithMember를 어떻게 만들 수 있을지 고민해 봅시다.방법.1 서비스에서 Team 정보, Member 정보를 Repository로부터 모두 불러와 TeamWithMembers 도메인을 만들어 사용한다.이는 즉 아래와 같은 코드를 만들겠다는 의미입니다.class MyService { // 의존성 주입되는 코드 생략 public void doSomething() { Team team = teamRepository.getById(teamId); List members = memberRepository.findByTeamId(teamId); TeamWithMember teamWithMember = TeamWithMember.builder() .id(team.getId()) .name(team.getName()) .members(members) .build(); // ... } } 이 방법을 사용하면 트랜잭션 스크립트 같은 코드가 만들어질 수 있습니다. 그것 외에는 특별히 문제가 될 만한 점은 보이지 않습니다. 이러한 역할을 수행하는 것이 서비스의 역할인 것인지는 약간 의문이 들긴 하나 그렇다고 또 완전히 틀린 역할은 아닙니다. 추가로 염려되는 점이라면 코드를 이렇게 작성하다 보면 결국 TeamWithMembers를 만들어주는 역할을 전문적으로 하는 서비스가 만들어 질 수 있다는 것입니다.방법.2 리포지토리에서 Team 정보, Member 정보를 Repository로부터 모두 불러와 TeamWithMembers 도메인을 만들어 사용한다.이는 즉 아래와 같은 코드를 만들겠다는 의미입니다.class TeamWithMembersRepositoryImpl implements TeamWithMembersRepository { // 의존성 주입되는 코드 생략 public TeamWithMembers getById(long teamId) { Team team = teamJpaRepository.getById(teamId); List members = memberJpaRepository.findByTeamId(teamId); return TeamWithMember.builder() .id(team.getId()) .name(team.getName()) .members(members) .build(); } } 크게 문제없어 보입니다.종합하면 이야기해 주신 상황은 Repository의 역할로 보입니다. 왜냐하면 Repository의 역할은 도메인 데이터를 저장하고 불러오는 것인데, TeamWithMembersRepositoryImpl은 그 역할에 정확히 부합한 일을 수행하고 있기 때문입니다.사실 이 문제는 의존성 역전만 제대로 돼있다면 방법.1로 개발하던 방법.2로 개발하던 크게 상관이 없는 문제입니다. 그래서 개발자마다 해석이 다를 수 있고 저는 방법.2가 맞다 생각하지만, 누군가는 방법.1이 맞다 생각할 수도 있습니다.헷갈린다면 어느날 갑자기 Jpa가 갑자기 사라진다고 생각해 보길 바랍니다. 그리고 “Jpa가 아닌 JdbcTemplate으로 대체해야 할 때 Domain, Service 코드를 안 건드려도 되는가?”를 고민해 보시길 바랍니다. 그리고 이 상황에서 방법.1이든 방법.2든 모두 안전합니다.답변에 도움이 됐길 바랍니다.
- 0
- 2
- 710
질문&답변
복사 단축키
안녕하세요. 너무 간단한 질문인데도 답변이 이리 늦어 정말 죄송합니다.multi cursor라는 기능입니다! mac에선 option + shift + click이고 window에선 alt + shift + click이라고 하네요. 다음 링크 참고하시면 도움 될 겁니다! (참조: https://www.jetbrains.com/help/idea/multicursor.html?keymap=windows)
- 0
- 2
- 445
질문&답변
List<Domain> -> List<Response> 변환을 Controller에서 하는 게 맞나요?
안녕하세요. 근래에 책을 집필할 기회가 생겨 그쪽에 힘을 실어주다 보니 다른 일에 신경 쓰지 못했습니다. 답변이 늦어 죄송합니다. 다만 해당 강의는 공식적으로 질의응답을 제공하지 않는 강의였다는 점을 이유로 늦어진 부분에 대해 양해 부탁드립니다.질문을 제가 정확히는 이해하지 못해서 일단 교정해야 할 부분과 궁금해하시는 것 같은 내용을 아래와 같이 나눠 답변드립니다.교정.1 우선 Domain → Response 로직은 Reponse가 들고 있습니다. (참조: https://github.com/kok202/test-code-with-architecture/blob/v2.0/src/main/java/com/example/demo/post/controller/response/PostResponse.java) 확인해 주시면 좋을 것 같고요.교정.2 Controller는 Domain 메서드를 호출하지 않습니다. 서비스를 호출하고 도메인을 가져와 Response의 ‘도메인 → Reponse’ 변환 메서드를 호출합니다. (참조: https://github.com/kok202/test-code-with-architecture/blob/73453defa54cd3d7d2288814ca1695a0cd3c0a8d/src/main/java/com/example/demo/post/controller/PostController.java#L30)다음으로 List을 List로 변환하고 싶은 경우이런 경우 말씀하신 것처럼 변환 로직이 Controller에 있으면 Controller 코드도 지저분해지고 Controller가 하는 역할에 부합하지 않는 일을 하게 됩니다. 그래서 다음과 같은 방법이 있습니다.일급 컬렉션을 만듭니다.이는 List보다 Domains라는 클래스를 만들어 사용하고 List보다 Responses라는 클래스를 만들어 사용한다는 의미입니다. 그리고 복수 개의 도메인을 복수 개의 Response로 변환하고 싶을 때 Responses.from(domains)를 호출해 변환하도록 합니다.일급 컬렉션과 관련된 자세한 내용은 다음 링크를 참조하는 게 좋아 보입니다. (참조: https://jojoldu.tistory.com/412, 질문과 유사한 사례는 PayGroups 사례를 참고해 해결하면 좋겠네요.)View / Render로 나눕니다. 이건 개인적으로 레거시 프로젝트에 많이 사용하는 방법입니다. Response를 View라 부르고 도메인을 이용해 View를 만들어주는 클래스를 Render라 부릅니다. (명칭은 사람마다 다를 수 있습니다. 다만 개인적으론 이렇게 부르는 게 제일 나았습니다. 이렇게 명명한 이유는 우리가 RestAPI 서버를 만들더라도 API 응답을 내려주는 것이 결국 옛날에 MVC 패턴으로 JSP view를 만들어주는 것과 크게 다를 것 없다 보기 때문입니다.)그래서 이 경우는 문제 해결이 더 간단합니다. Domain → View 변환 로직을 모두 Render가 들고 있게 하는 겁니다. 그러면 이런 코드가 만들어지겠네요. PostRender.render(posts) 개인적으론 1번 방법이 객체 지향적으로 좀 더 맞는 방향이라 생각하기 때문에 이를 더 선호합니다.답변에 도움 됐길 바라며, 감사합니다.
- 0
- 2
- 732
질문&답변
SystemUuidHolder를 테스트하는 경우
안녕하세요. 근래에 책을 집필할 기회가 생겨 그쪽에 힘을 실어주다 보니 다른 일에 신경 쓰지 못했습니다. 답변이 늦어 죄송합니다. 다만 해당 강의는 공식적으로 질의응답을 제공하지 않는 강의였다는 점을 이유로 늦어진 부분에 대해 양해 부탁드립니다.질문이 2개인 것으로 이해했습니다.하나는 SystemUuidHolder도 테스트해야 하는 것 아닌가?또 다른 하나는 테스트는 FakeUuidHolder로 했더라도 결국 시스템이 실환경(Production)에 나가면 SystemUuidHolder가 사용될 테니 이에 대한 테스트는 다시 해야 하는 것 아닌가?그래서 이를 바탕으로 각각에 대해 답변드립니다. SystemUuidHolder도 테스트해야 하는 것 아닌가?적어도 SystemUuidHolder의 동작 자체는 굳이 테스트하지 않아도 됩니다. 왜냐하면 UUID 클래스의 동작은 너무나 신뢰할 수 있기 때문입니다. 마찬가지로 Clock 클래스의 동작도 너무나 신뢰할 수 있기 때문에 SystemUuidHolder나 SystemClockHolder의 동작이 불완전할 가능성은 거의 0에 가깝습니다.더불어 UUID나 Clock의 테스트는 UUID 클래스와 Clock 클래스를 만든 JDK 개발팀의 역할입니다. 그러니 UUID, Clock 메서드를 중개해 줄 뿐인 SystemUuidHolder, SystemClockHolder을 굳이 테스트할 필요는 없습니다. 따라서 실제 구현체를 테스트할지 말지 여부는 결국 ‘실제 구현체의 동작이 얼마나 신뢰할 수 있는가?’를 기준으로 판단해주면 됩니다. 테스트는 FakeUuidHolder로 했더라도 결국 시스템이 실환경(Production)에 나가면 SystemUuidHolder가 사용될 테니 이에 대한 테스트는 다시 해야 하는 것 아닌가?이 내용은 강의 내에도 일부 포함된 것으로 아는데요. 1번 답변과 마찬가지로 SystemUuidHolder, SystemClockHolder의 동작은 확신할 수 있기 때문에 이 두 개는 test double이 사용된 테스트로도 충분하다 봅니다. 그런데 일부 라이브러리나 외부 시스템의 실 환경(Production)이나 준 실 환경(CBT)에서 다른 동작을 보일 수 있기 때문에 눈으로 보고 확인해야 하는 경우도 많습니다. 그런 경우에는 어쩔 수 없습니다. 수동 테스트나 대형 테스트를 만들어 테스트해야 합니다. (그런데 결국 이 주제는 mockist vs classicist입니다.) 답변이 도움 됐길 바랍니다. 감사합니다.
- 0
- 2
- 449
질문&답변
FakeRepository 만들 때 Join 테이블하는 경우 질문드립니다.
안녕하세요. 근래에 책을 집필할 기회가 생겨 그쪽에 힘을 실어주다 보니 다른 일에 신경 쓰지 못했습니다. 답변이 늦어 죄송합니다. 다만 해당 강의는 공식적으로 질의응답을 제공하지 않는 강의였다는 점을 이유로 늦어진 부분에 대해 양해 부탁드립니다.우선 Join을 통해 데이터를 불러오는 예시부터 보여드리겠습니다. ‘게시물 ↔ 댓글’이라는 관계가 있을 때 댓글에는 postId가 적혀있고, 게시물을 불러올 때 댓글도 같이 불러오고 싶다고 가정합시다. 이때 Fake는 아래와 같이 만들 수 있습니다.public class FakePostWithCommentRepository implements PostWithCommentRepository { private final FakePostRepository fakePostRepository; private final FakeCommentRepository fakeCommentRepository; public FakePostWithCommentRepository( FakePostRepository fakePostRepository, FakeCommentRepository fakeCommentRepository) { this.fakePostRepository = fakePostRepository; this.fakeCommentRepository = fakeCommentRepository; } @Override public Optional findById(long id) { return fakePostRepository.findById() .map(post -> PostWithComment.builder() .post(post) .comments(fakeCommentRepository.findByPostId()) .build()); } } 코드 aJoin이 사용되는 Fake를 만들 때 Join에 필요한 데이터 소스를 갖고 있는 Repository를 생성자 주입받고 이를 활용하도록 하는 겁니다.아무튼 Join을 거는 상황 자체는 위와 같이 해결할 수 있는데, 문의해 주신 질문을 읽었을 때 질문자님이 궁금한 것은 단순히 Join을 거는 상황에서 데이터를 어떻게 불러오는지가 궁금한 것 같지 않습니다.오히려 그것보다 Fake가 있는 경우 데이터 정합성을 어떻게 보장할 수 있는지가 궁금한 것 같은데요. 예를 들면 예제 프로젝트에서 사용했던 아래 두 Fake Repository를 한 번 다시 보고 와봅시다.https://github.com/kok202/test-code-with-architecture/blob/v2.0/src/test/java/com/example/demo/mock/FakePostRepository.javahttps://github.com/kok202/test-code-with-architecture/blob/v2.0/src/test/java/com/example/demo/mock/FakeUserRepository.javaFakePostRepository에서 Writer는 FakeUserRepository의 data에 있는 값이 아닙니다. 그래서 FakeUserRepository에서 사용자 정보를 변경하더라도 FakePostRepository에 있는 게시물에서 Writer정보도 같이 변경되는 것이 아닙니다. 그리고 이는 명백히 정합성이 깨진 상황입니다.이런 경우 FakePostRepository가 FakeUserRepository를 [코드 a]와 마찬가지로 FakePostRepository가 FakeUserRepository를 갖고 있게 함으로써 문제를 해결할 수 있습니다. 다만 굉장히 많은 것을 고려해야 하고 Fake의 내부 코드 자체가 굉장히 지저분해지기 시작할 겁니다.그래서 결국 RDB를 사용하고 있는 상황에서 테이블에 연관 관계가 복잡하게 얽혀있는 경우 완전한 Fake를 만들어 이를 대체하도록 하는 것은 무척이나 어려운 일입니다. 사실상 RDB의 모든 기능을 Fake로 완전히 대체하겠다는 목표도 달성할 수 없는 목표에 가깝고요.본 강의의 목표는 테스트 프레임워크나 h2 사용 자체를 하지 말자는 것을 지향하는 것이 아닙니다. 적어도 쓸 땐 쓰더라도 테스트 프레임워크가 어떤 일을 도와주고 있고, h2 없이도 어떻게 테스트를 만들 수 있는지를 알려드리는 것을 목표로 하고 있습니다. 그러므로 상황이 정말로 복잡하다면 h2나 stub을 활용해도 좋은 것 같습니다!마지막으로 개인적으론 Fake를 만들기 쉬울 만큼 객체나 테이블 관계를 복잡하게 안 만들려 노력합니다.답변이 도움 됐길 바랍니다.
- 1
- 2
- 739
질문&답변
inteface UserRepsotiroy 를 service 패키지로 뺀 이유에대해서 궁금합니다.
안녕하세요. 근래에 책을 집필할 기회가 생겨 그쪽에 힘을 실어주다 보니 다른 일에 신경 쓰지 못했습니다. 답변이 늦어 죄송합니다. 다만 해당 강의는 공식적으로 질의응답을 제공하지 않는 강의였다는 점을 이유로 늦어진 부분에 대해 양해 부탁드립니다.우선 저의 안 좋은 습관이 옮은 것 같아 먼저 정정해 드립니다. service 레이어가 아니라 applicaiton 레이어라 부르는 것이 맞습니다. 제가 applicaiton 레이어를 service 레이어라 부르는 습관이 있는데, 이게 강의에 그대로 찍혀있더라고요. 최대한 빨리 수정하겠습니다;;질문 주신 내용에 관해서는 후속으로 달아주신 댓글처럼 이해해 주신 내용이 맞습니다. UserRepository가 infrastructure에 있다면 applicaiton 레이어가 infrastructure 레이어에 의존하는 그림이 됩니다. 이를 피하기 위해 UserRepository를 applicaiton 레이어에 위치시킨 것입니다.덧으로 레이어의 상하 관계는 고정적이지 않습니다. 전통적인 레이어드 아키텍처를 추구하는 누군가는 infrastructure 레이어를 최하위 레이어로 봅니다. (참조: https://www.oreilly.com/library/view/software-architecture-patterns/9781491971437/ch01.html 여기서는 infrastructure 레이어를 persistence layer라고 보네요) 한편 헥사고날 아키텍처를 추구하는 또 다른 누군가는 infrastructure 레이어를 최상위 레이어로 봅니다. (참조: https://medium.com/@luishrsoares/whats-hexagonal-architecture-6da22d4ab600 여기서 infrastructure 레이어가 가장 바깥에 존재한다는 것을 유심히 봐주세요.)그러니 일단 레이어드 아키텍처의 틀에서 벗어나길 추천드립니다. 전통적인 레이어드 아키텍처는 좀 이상한 부분이 많습니다.+) 소프트웨어 설계를 설명하는데 통용될 수 있는 단 하나의 원칙 같은 게 있는 것은 아닙니다. 예를 들어 “상위 레이어는 하위 레이어를 참조해선 안 된다.”라는 원칙 하나만으로 소프트웨어를 이해하려 한다 가정해 봅시다. 그런데 이 원칙을 본 강의에서 설명하는 아키텍처에 적용하려 보면 많은 모순이 있습니다.상위 레이어에 위치한 application 레이어가 하위 레이어에 위치한 domain 레이어를 참조하고 있습니다. 또 한편 하위 레이어에 위치한 infrastructure 레이어가 상위 레이어인 application 레이어를 참조하게 만들고 있고요. (물론 이는 레이어드 아키텍처를 헥사고날 스타일에 맞게 재정렬하면서 해소가 됩니다.)답변이 도움 됐길 바랍니다.
- 2
- 3
- 662