Feat: 헥사고날 아키텍쳐 기반 Auth 및 OAuth 로직 구현 (NEWZET 경험 기반 고도화)#17
Conversation
📊 Code Coverage Report
|
Test Results161 tests 161 ✅ 5s ⏱️ Results for commit 313892b. ♻️ This comment has been updated with latest results. |
Dockerel
left a comment
There was a problem hiding this comment.
코드 잘 봤습니다.
전체적으로 확장성 있게 잘 짜주신 것 같습니다!
다만 제가 이 아키텍처에 익숙하지 않아서 각 패키지 간에 어떤 관계를 가지고 있는지, 그리고 왜 dto로 변환해서 사용하는지 등에 대해 아직은 잘 모르겠습니다.
이런 아키텍처를 사용해보신 입장에서 어떤 장점이 있고 도입할만한 이유가 뭔지 알고싶습니다.
| String refreshToken, | ||
| long expiresIn) { | ||
|
|
||
| public static OAuthTokenDto from(OAuthToken oAuthToken) { |
There was a problem hiding this comment.
혹시 from을 사용하신 이유가 있으신가요? 제 생각엔 from은 입력값에 가공이나 변환이 필요한 경우에 사용하고 of는 그대로 입력값을 받아 생성할 때 사용하는게 자연스럽다 생각하는데 혹시 어떻게 생각하시나요?
There was a problem hiding this comment.
저는 from은 단일 인자값을 기반으로, of는 여러 인자값을 기반으로 static 생성자를 구성할 때 사용하는걸로 이해하여 사용했습니다. 기헌님같은 경우에는 위 코드의 경우 of로 구성하는것이 좀 더 자연스럽다고 생각하시는걸까요?
There was a problem hiding this comment.
아하 넵 그렇군요
https://docs.oracle.com/javase/tutorial/datetime/overview/naming.html
자바 네이밍 컨벤션에 이런 기준이 있긴 한데 또 다른 분들 의견을 참고해보니 적절하다 생각하는 네이밍을 사용하는 것이 더 중요한 것 같긴 하네요!
그냥 의미상 자연스러운 정적 펙토리 메서드로 만들어 쓰면 될 것 같습니다~!
| import org.springframework.web.bind.annotation.PathVariable; | ||
| import org.springframework.web.bind.annotation.RequestParam; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
| import org.springframework.web.servlet.view.RedirectView; |
There was a problem hiding this comment.
아하 이걸로 외부 url로 리다이렉트 할 수 있군요
| ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage()); | ||
| problemDetail.setTitle("OAuth Bad Request"); |
There was a problem hiding this comment.
이 부분들을 공통 로직으로 분리해도 괜찮을 것 같습니다!
There was a problem hiding this comment.
네네 좋습니다 :) 일단은 이 부분처럼 예외처리같은 경우에 기헌님께서 좀 더 좋은 아이디어를 가지고 계실 것 같아서 우선적으로는 가장 임시의 방법으로 적용한 상태입니다
|
|
||
| String getNickname(); | ||
|
|
||
| boolean isWithdrawn(); |
There was a problem hiding this comment.
아직 실제 구현은 하지 않았지만, 실제 운영을 고려하였을 때 탈퇴한 유저의 정보 또한 서비스의 중요 자원이라고 생각하여 탈퇴한 사용자의 정보도 db내에 유지하고자 간단하게 구조를 설계해보았습니다. 실제로 많은 서비스들이 탈퇴한 유저나 휴면 유저의 정보를 사전 약속된 기간동안 가지고 있다고 하네요..!
There was a problem hiding this comment.
오우 그렇군요. 찾아보니 목적에 따라 6개월에서 길게는 5년까지도 가지고 있을 수 있다고 하네요 👀
|
|
||
| @Getter | ||
| @AllArgsConstructor(access = AccessLevel.PRIVATE) | ||
| public class UserDomain implements User { |
There was a problem hiding this comment.
아하 엔티티는 디비에 저장되는 용도고 도메인은 엔티티 관련 비즈니스 로직이 포함된 클래스라 이해했는데 맞을까요?
There was a problem hiding this comment.
넵 맞습니다! 엔티티 자체가 JPA에 의존적이라 DB 매핑이나 데이터 저장 조회와 같은 로직에 집중하고, 도메인은 DDD관점에서의 비즈니스 규칙이 정의된 객체로 JPA와 같은 외부 의존성이 없는 순수 객체로 유지되어 엔티티와 다른 책임을 가지고 있습니다
| @UuidGenerator | ||
| @Column(columnDefinition = "char(36)", updatable = false, nullable = false) | ||
| @JdbcTypeCode(SqlTypes.CHAR) | ||
| private UUID id; |
There was a problem hiding this comment.
이 부분에 대해서 UUID가 저장되는 시점에 하이버네이트가 자동으로 uuid를 자동 생성해서 id를 채워주는 것 같은데, 혹시 그럼 새로운 엔티티 생성 시 새로운 엔티티로 판정되나요?
만약 isNew()로 새로운 엔티티로 판정되지 않으면 불필요한 디비 조회가 발생할 수 있을 것 같습니다.
기존 방식대로 저장 직전까지 null로 있다가 저장 시점에서 생성되는거면 상관없을 것 같긴 합니다.
There was a problem hiding this comment.
너무 좋은 접근이네요!
덕분에 관련해서 공부해보고 내용 블로그에 작성해두었습니다 :)
UUID와 JPA의 숨겨진 함정: isNew() 판정과 성능 최적화 가이드
결론적으로는 uuidGenerator가 id를 지연 생성하여 추가적인 쿼리 없이 작동합니다 👍
| UserEntityDto userEntityDto = userRepository.save(request.email(), request.nickname(), | ||
| UserStatus.ACTIVE.name()); | ||
| User user = userEntityDto.toDomain(); |
There was a problem hiding this comment.
제가 이런 아키텍처가 처음이라 유저 엔티티를 유저 엔티티 dto로 바꾸고 다시 그걸 유저 도메인으로 바꾸고 하는게 너무 오버 엔지니어링 같은데 혹시 이렇게 하면 어떤 장점이 있나요?
There was a problem hiding this comment.
완전한 의존성 격리를 통해 DIP를 구현할 수 있습니다. 이는 나중에 infra단에서 JPA를 MyBatis나 MongoDB등으로 교체하여도 Domain과 Business 계층은 전혀 수정이 불필요해진다는 장점을 가져올 수 있습니다..!
또한, 이후 비활성유저, 유저 개인정보등 User 도메인 자체가 복잡해지는 경우에도 도메인단의 비즈니스 로직만 추가한 후 dto를 통해 소통하여 entity는 단순하게 유지할 수 있다는 장점이 있습니다. OAuthMapping같은 경우를 봐도 도메인단이 분리되어있고, 비즈니스 로직이 많이지만 entity단과 dto를 통해 소통하면서 entity은 단순하게 유지가 가능했습니다.
그러나, 말씀하신 것처럼 현재의 프로젝트 규모에서는 오히려 비즈니스 로직보다 DTO 변환 로직이 많이지는 문제가 생길 수 있을 것 같아서 간단한 도메인은 DTO변환 로직을 생략하고 Entity - Domain단의 직접 소통을 가능하게 하는 것도 고려해도 좋을 것 같네요!
|
|
||
| @RestController | ||
| @RequiredArgsConstructor | ||
| public class UserController { |
There was a problem hiding this comment.
여기에 implements UserApi가 들어가지 않나요? 일부러 아직 안해놓으신 건가요?
| @Operation(summary = "메일 중복조회", | ||
| description = "메일이 사용 가능한지 조회한다. 휴면유저/탈퇴한 유저의 메일도 사용 불가.") | ||
| public ResponseEntity<UniqueMailResponse> checkEmailUniqueness( | ||
| @RequestParam("v") String email) { |
There was a problem hiding this comment.
뉴젯에서 v라는 파라미터를 사용했어서 넣어두었습니다. 별다른 의미는 없어 나중에 프론트측과 협의 후에 파라미터 이름은 변경하는게 좋을 것 같네요!
@Dockerel 저도 처음에는 이해하기 어려웠는데, 사용해보니 확실한 장점들이 있었습니다. 제 생각에 가장 큰 장점은 의존성 역전입니다. Domain 계층이 프레임워크나 외부 기술에 의존하지 않아서, 나중에 JPA를 다른 기술로 바꾸거나 데이터베이스를 교체해도 핵심 비즈니스 로직은 전혀 수정하지 않아도 됩니다. DTO 변환은 각 계층 간의 안정적인 계약역할을 해서, 한 계층의 기술적 변경이 다른 계층에 영향을 주지 않도록 보호해준다고 생각합니다. 실제로 개발할 때도 새로운 기능을 추가하기가 편해졌습니다. 예를 들어 OAuth Provider를 Kakao 뿐만 아니라 google이나 naver 등 추가적인 provider를 추가할 때 기존 코드를 수정할 필요가 없고, 각 계층을 독립적으로 테스트할 수 있어서 디버깅도 훨씬 쉬워졌습니다. 다만 소규모 프로젝트에서는 다소 복잡할 수 있다는 점은 완전히 동의합니다,, 그러나 ddip 서비스 특성상 지속적인 개발이나 운영이 타 프로젝트에 비해 활발히 진행될 것 같다고 생각하고 이런 기회에 확장성 좋은 아키텍쳐를 도입하는게 어떨까 싶네요..! |
Dockerel
left a comment
There was a problem hiding this comment.
좋습니다!
아키텍처를 보다 보니 서로 합당한 이유로 분리와 연결을 잘 나타내고 있는 것으로 보이네요.
이대로 한번 진행해보시죠!
코드 작성하느라 고생많으셨습니다 👍
#️⃣ 연관된 이슈
📚 배경
기존 NEWZET 프로젝트에서 구현했던 객체지향적 JWT 인증 처리와 확장 가능한 OAuth 구조를 베이스로, 더욱 발전된 헥사고날 아키텍처를 도입했습니다. PostgreSQL에서 MySQL로의 마이그레이션, DeviceType 재정의, API 인터페이스 분리 등을 통해 이전 프로젝트의 한계점들을 보완한 백엔드 시스템으로 진화시켰습니다.
뉴젯의 작업물
[NEWZET - AUTH 1탄] 객체지향적 JWT 인증 처리 및 커스텀 리졸버/인터셉터 도입
[NEWZET - AUTH 2탄] 확장 가능한 객체지향적 OAuth 구조 설계 및 카카오 소셜 로그인 구현
📝 작업 내용
1. DeviceType 전략적 재설계
변경사항: Web/App → TABLET/PHONE
기존: 플랫폼 기반 구분 (웹/앱)
개선: 디바이스 형태 기반 구분 (태블릿/폰)
이유: 반응형 웹 시대에 맞는 더 명확한 사용자 경험 구분
2. 헥사고날 아키텍쳐 패턴 고도화 구현
3. MySQL 마이그레이션 및 UUID 최적화
해결된 이슈:
4. API 설계 Interface Segregation 패턴 도입
Feat: OAuth관련 엔드포인트 swagger 설정을 위한 인터페이스 구성
Feat: User용 presentation 단에서 사용할 swagger및 rest 설정 인터페이스 구성
5. 테스트 인프라 개선
Test: Redis 테스트 컨테이너 설정
Test: MySQL의 테스트 컨테이너를 primary로 테스트 환경에서 설정
Test: Redis의 테스트 컨테이너를 primary로 테스트 환경에서 설정
Test: gitignore된 환경변수들을 테스트환경에서 임의의 값으로 사용할 수 있도록 구성
주요 항목:
-> 실제 DB를 사용하지 않고 통일된 환경에서 테스트 가능
💬 리뷰 요구사항
이해가 안되는 부분이 있으시거나, 전체적인 플로우가 궁금하시다면 편하게 말씀해주세요
✏ Git Close
close #16