Skip to content

Feat: 헥사고날 아키텍쳐 기반 Auth 및 OAuth 로직 구현 (NEWZET 경험 기반 고도화)#17

Merged
GitJIHO merged 71 commits intomainfrom
Feat/issue-#16
Aug 2, 2025
Merged

Feat: 헥사고날 아키텍쳐 기반 Auth 및 OAuth 로직 구현 (NEWZET 경험 기반 고도화)#17
GitJIHO merged 71 commits intomainfrom
Feat/issue-#16

Conversation

@GitJIHO
Copy link
Contributor

@GitJIHO GitJIHO commented Aug 1, 2025

#️⃣ 연관된 이슈

📚 배경

기존 NEWZET 프로젝트에서 구현했던 객체지향적 JWT 인증 처리와 확장 가능한 OAuth 구조를 베이스로, 더욱 발전된 헥사고날 아키텍처를 도입했습니다. PostgreSQL에서 MySQL로의 마이그레이션, DeviceType 재정의, API 인터페이스 분리 등을 통해 이전 프로젝트의 한계점들을 보완한 백엔드 시스템으로 진화시켰습니다.

뉴젯의 작업물
[NEWZET - AUTH 1탄] 객체지향적 JWT 인증 처리 및 커스텀 리졸버/인터셉터 도입
[NEWZET - AUTH 2탄] 확장 가능한 객체지향적 OAuth 구조 설계 및 카카오 소셜 로그인 구현

📝 작업 내용

기존 NEWZET 프로젝트 대비 주요 개선사항

1. DeviceType 전략적 재설계

변경사항: Web/App → TABLET/PHONE
기존: 플랫폼 기반 구분 (웹/앱)
개선: 디바이스 형태 기반 구분 (태블릿/폰)
이유: 반응형 웹 시대에 맞는 더 명확한 사용자 경험 구분

2. 헥사고날 아키텍쳐 패턴 고도화 구현

DDIP 아키텍처 구조
├── business
│   ├── service/ - 비즈니스 로직 오케스트레이션
│   ├── port/in/ - 인바운드 포트 (Controller가 호출)
│   └── port/out/ - 아웃바운드 포트 (Repository 추상화)
├── domain (순수 비즈니스 로직)
│   ├── 도메인 엔티티 (User, Token, OAuthMapping)
│   ├── 값 객체 (DeviceType, OAuthProvider)
│   └── 도메인 인터페이스
├── Infrastructure
│   ├── JPA Repository 구현체
│   ├── external/ - 외부 API (카카오, Redis)
│   └── entity/ - JPA 엔티티
└── presentation
    ├── controller/ - HTTP 요청 처리
    ├── api/ - API 명세 인터페이스 (Swagger 분리)
    └── interceptor/ - 인증/인가 처리

3. MySQL 마이그레이션 및 UUID 최적화

// PostgreSQL (기존)
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;

// MySQL (개선)
@Id
@UuidGenerator
@Column(columnDefinition = "char(36)", updatable = false, nullable = false)
@JdbcTypeCode(SqlTypes.CHAR)
private UUID id;

해결된 이슈:

  • UUID 타입 호환성 문제 해결
  • char(36) 타입으로 인덱스 성능 최적화
  • MySQL 8.0 호환성 보장

4. API 설계 Interface Segregation 패턴 도입

Feat: OAuth관련 엔드포인트 swagger 설정을 위한 인터페이스 구성

Feat: User용 presentation 단에서 사용할 swagger및 rest 설정 인터페이스 구성

// 기존: Controller에 Swagger 어노테이션 직접 추가 (관심사 혼재)
@RestController
@Tag(name = "유저")
public class UserController {
    @PostMapping("/signup")
    @Operation(summary = "회원가입")
    public ResponseEntity<JwtResponse> signup() { ... }
}

// 개선: API 명세와 구현체 완전 분리
@Tag(name = "유저", description = "유저 관련 API")
@RequestMapping("/api")
public interface UserApi {
    @Operation(summary = "회원 가입", description = "OAuth 후 회원가입 진행")
    ResponseEntity<JwtResponse> signup(@Valid @RequestBody SignupRequest request);
}

@RestController
@RequiredArgsConstructor
public class UserController implements UserApi {
    // 순수한 비즈니스 로직만 집중
}

5. 테스트 인프라 개선

Test: Redis 테스트 컨테이너 설정

Test: MySQL의 테스트 컨테이너를 primary로 테스트 환경에서 설정

Test: Redis의 테스트 컨테이너를 primary로 테스트 환경에서 설정

Test: gitignore된 환경변수들을 테스트환경에서 임의의 값으로 사용할 수 있도록 구성

주요 항목:

  • TEST 하위 config에 MySQL, Redis 관련 컨테이너 구성
    -> 실제 DB를 사용하지 않고 통일된 환경에서 테스트 가능
  • primary bean 설정으로 테스트 환경에서만 작동하도록 구현
  • CI 환경에서의 env 제거

💬 리뷰 요구사항

이해가 안되는 부분이 있으시거나, 전체적인 플로우가 궁금하시다면 편하게 말씀해주세요

✏ Git Close

close #16

GitJIHO added 30 commits August 2, 2025 02:11
@GitJIHO GitJIHO requested a review from Dockerel August 1, 2025 18:36
@GitJIHO GitJIHO self-assigned this Aug 1, 2025
@GitJIHO GitJIHO added ✨ Feature 새로운 기능 추가 및 구현하는 경우 ✅ Test Code 테스트 관련 작업을 진행하는 경우 labels Aug 1, 2025
@GitJIHO GitJIHO linked an issue Aug 1, 2025 that may be closed by this pull request
5 tasks
@github-actions
Copy link

github-actions bot commented Aug 1, 2025

📊 Code Coverage Report

Overall Project 92.77% -7.23% 🍏
Files changed 92.32% 🍏

File Coverage
WebConfig.java 100% 🍏
RedisConfig.java 100% 🍏
AuthUserArgumentResolver.java 100% 🍏
UserRepositoryImpl.java 100% 🍏
UserFactory.java 100% 🍏
UserEntityStatus.java 100% 🍏
RedisTokenRepositoryImpl.java 100% 🍏
KakaoUserInfoResponse.java 100% 🍏
KakaoTokenResponse.java 100% 🍏
KakaoOAuthService.java 100% 🍏
AuthInterceptor.java 100% 🍏
JwtFactory.java 100% 🍏
JwtService.java 100% 🍏
OAuthLoginService.java 100% 🍏
JwtValidator.java 94.95% -5.05% 🍏
UserService.java 90.48% -9.52% 🍏
OAuthMappingEntity.java 89.71% -10.29% 🍏
UserEntity.java 74.47% -25.53%
OAuthRepositoryImpl.java 61.43% -38.57%
OAuthController.java 0%
UserController.java 0%

@github-actions
Copy link

github-actions bot commented Aug 1, 2025

Test Results

161 tests   161 ✅  5s ⏱️
 25 suites    0 💤
 25 files      0 ❌

Results for commit 313892b.

♻️ This comment has been updated with latest results.

Copy link
Contributor

@Dockerel Dockerel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드 잘 봤습니다.

전체적으로 확장성 있게 잘 짜주신 것 같습니다!

다만 제가 이 아키텍처에 익숙하지 않아서 각 패키지 간에 어떤 관계를 가지고 있는지, 그리고 왜 dto로 변환해서 사용하는지 등에 대해 아직은 잘 모르겠습니다.

이런 아키텍처를 사용해보신 입장에서 어떤 장점이 있고 도입할만한 이유가 뭔지 알고싶습니다.

String refreshToken,
long expiresIn) {

public static OAuthTokenDto from(OAuthToken oAuthToken) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 from을 사용하신 이유가 있으신가요? 제 생각엔 from은 입력값에 가공이나 변환이 필요한 경우에 사용하고 of는 그대로 입력값을 받아 생성할 때 사용하는게 자연스럽다 생각하는데 혹시 어떻게 생각하시나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 from은 단일 인자값을 기반으로, of는 여러 인자값을 기반으로 static 생성자를 구성할 때 사용하는걸로 이해하여 사용했습니다. 기헌님같은 경우에는 위 코드의 경우 of로 구성하는것이 좀 더 자연스럽다고 생각하시는걸까요?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 넵 그렇군요

https://docs.oracle.com/javase/tutorial/datetime/overview/naming.html

자바 네이밍 컨벤션에 이런 기준이 있긴 한데 또 다른 분들 의견을 참고해보니 적절하다 생각하는 네이밍을 사용하는 것이 더 중요한 것 같긴 하네요!

그냥 의미상 자연스러운 정적 펙토리 메서드로 만들어 쓰면 될 것 같습니다~!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 좋은 자료 감사합니다 😄

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 이걸로 외부 url로 리다이렉트 할 수 있군요

Comment on lines +17 to +18
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage());
problemDetail.setTitle("OAuth Bad Request");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분들을 공통 로직으로 분리해도 괜찮을 것 같습니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네네 좋습니다 :) 일단은 이 부분처럼 예외처리같은 경우에 기헌님께서 좀 더 좋은 아이디어를 가지고 계실 것 같아서 우선적으로는 가장 임시의 방법으로 적용한 상태입니다


String getNickname();

boolean isWithdrawn();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

탈퇴한 사용자의 정보도 갖고있는건가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아직 실제 구현은 하지 않았지만, 실제 운영을 고려하였을 때 탈퇴한 유저의 정보 또한 서비스의 중요 자원이라고 생각하여 탈퇴한 사용자의 정보도 db내에 유지하고자 간단하게 구조를 설계해보았습니다. 실제로 많은 서비스들이 탈퇴한 유저나 휴면 유저의 정보를 사전 약속된 기간동안 가지고 있다고 하네요..!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오우 그렇군요. 찾아보니 목적에 따라 6개월에서 길게는 5년까지도 가지고 있을 수 있다고 하네요 👀


@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class UserDomain implements User {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 엔티티는 디비에 저장되는 용도고 도메인은 엔티티 관련 비즈니스 로직이 포함된 클래스라 이해했는데 맞을까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 맞습니다! 엔티티 자체가 JPA에 의존적이라 DB 매핑이나 데이터 저장 조회와 같은 로직에 집중하고, 도메인은 DDD관점에서의 비즈니스 규칙이 정의된 객체로 JPA와 같은 외부 의존성이 없는 순수 객체로 유지되어 엔티티와 다른 책임을 가지고 있습니다

@UuidGenerator
@Column(columnDefinition = "char(36)", updatable = false, nullable = false)
@JdbcTypeCode(SqlTypes.CHAR)
private UUID id;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분에 대해서 UUID가 저장되는 시점에 하이버네이트가 자동으로 uuid를 자동 생성해서 id를 채워주는 것 같은데, 혹시 그럼 새로운 엔티티 생성 시 새로운 엔티티로 판정되나요?

만약 isNew()로 새로운 엔티티로 판정되지 않으면 불필요한 디비 조회가 발생할 수 있을 것 같습니다.

기존 방식대로 저장 직전까지 null로 있다가 저장 시점에서 생성되는거면 상관없을 것 같긴 합니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

너무 좋은 접근이네요!
덕분에 관련해서 공부해보고 내용 블로그에 작성해두었습니다 :)
UUID와 JPA의 숨겨진 함정: isNew() 판정과 성능 최적화 가이드

결론적으로는 uuidGenerator가 id를 지연 생성하여 추가적인 쿼리 없이 작동합니다 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

크 고민 해결해주셔서 감사합니다 👍

Comment on lines +58 to +60
UserEntityDto userEntityDto = userRepository.save(request.email(), request.nickname(),
UserStatus.ACTIVE.name());
User user = userEntityDto.toDomain();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 이런 아키텍처가 처음이라 유저 엔티티를 유저 엔티티 dto로 바꾸고 다시 그걸 유저 도메인으로 바꾸고 하는게 너무 오버 엔지니어링 같은데 혹시 이렇게 하면 어떤 장점이 있나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

완전한 의존성 격리를 통해 DIP를 구현할 수 있습니다. 이는 나중에 infra단에서 JPA를 MyBatis나 MongoDB등으로 교체하여도 Domain과 Business 계층은 전혀 수정이 불필요해진다는 장점을 가져올 수 있습니다..!

또한, 이후 비활성유저, 유저 개인정보등 User 도메인 자체가 복잡해지는 경우에도 도메인단의 비즈니스 로직만 추가한 후 dto를 통해 소통하여 entity는 단순하게 유지할 수 있다는 장점이 있습니다. OAuthMapping같은 경우를 봐도 도메인단이 분리되어있고, 비즈니스 로직이 많이지만 entity단과 dto를 통해 소통하면서 entity은 단순하게 유지가 가능했습니다.

그러나, 말씀하신 것처럼 현재의 프로젝트 규모에서는 오히려 비즈니스 로직보다 DTO 변환 로직이 많이지는 문제가 생길 수 있을 것 같아서 간단한 도메인은 DTO변환 로직을 생략하고 Entity - Domain단의 직접 소통을 가능하게 하는 것도 고려해도 좋을 것 같네요!


@RestController
@RequiredArgsConstructor
public class UserController {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기에 implements UserApi가 들어가지 않나요? 일부러 아직 안해놓으신 건가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

헉 깜박했네요! 감사합니다 👍

@Operation(summary = "메일 중복조회",
description = "메일이 사용 가능한지 조회한다. 휴면유저/탈퇴한 유저의 메일도 사용 불가.")
public ResponseEntity<UniqueMailResponse> checkEmailUniqueness(
@RequestParam("v") String email) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

v는 무슨 뜻인가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

뉴젯에서 v라는 파라미터를 사용했어서 넣어두었습니다. 별다른 의미는 없어 나중에 프론트측과 협의 후에 파라미터 이름은 변경하는게 좋을 것 같네요!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

크 테스트용 프로퍼티 좋습니다 👍

@GitJIHO
Copy link
Contributor Author

GitJIHO commented Aug 2, 2025

다만 제가 이 아키텍처에 익숙하지 않아서 각 패키지 간에 어떤 관계를 가지고 있는지, 그리고 왜 dto로 변환해서 사용하는지 등에 대해 아직은 잘 모르겠습니다.
이런 아키텍처를 사용해보신 입장에서 어떤 장점이 있고 도입할만한 이유가 뭔지 알고싶습니다.

@Dockerel 저도 처음에는 이해하기 어려웠는데, 사용해보니 확실한 장점들이 있었습니다.

제 생각에 가장 큰 장점은 의존성 역전입니다. Domain 계층이 프레임워크나 외부 기술에 의존하지 않아서, 나중에 JPA를 다른 기술로 바꾸거나 데이터베이스를 교체해도 핵심 비즈니스 로직은 전혀 수정하지 않아도 됩니다. DTO 변환은 각 계층 간의 안정적인 계약역할을 해서, 한 계층의 기술적 변경이 다른 계층에 영향을 주지 않도록 보호해준다고 생각합니다.

실제로 개발할 때도 새로운 기능을 추가하기가 편해졌습니다. 예를 들어 OAuth Provider를 Kakao 뿐만 아니라 google이나 naver 등 추가적인 provider를 추가할 때 기존 코드를 수정할 필요가 없고, 각 계층을 독립적으로 테스트할 수 있어서 디버깅도 훨씬 쉬워졌습니다. 다만 소규모 프로젝트에서는 다소 복잡할 수 있다는 점은 완전히 동의합니다,, 그러나 ddip 서비스 특성상 지속적인 개발이나 운영이 타 프로젝트에 비해 활발히 진행될 것 같다고 생각하고 이런 기회에 확장성 좋은 아키텍쳐를 도입하는게 어떨까 싶네요..!

Copy link
Contributor

@Dockerel Dockerel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋습니다!

아키텍처를 보다 보니 서로 합당한 이유로 분리와 연결을 잘 나타내고 있는 것으로 보이네요.
이대로 한번 진행해보시죠!

코드 작성하느라 고생많으셨습니다 👍

@GitJIHO GitJIHO merged commit 994553c into main Aug 2, 2025
2 checks passed
@GitJIHO GitJIHO deleted the Feat/issue-#16 branch August 2, 2025 12:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 새로운 기능 추가 및 구현하는 경우 ✅ Test Code 테스트 관련 작업을 진행하는 경우

Projects

None yet

Development

Successfully merging this pull request may close these issues.

객체지향적 JWT 인증처리 및 OAuth 도입

2 participants