[Java Spring] COGO 개발일지 - 코드 중복 해결에 대한 고민 해결책 (DI, AOP, 어노테이션)

2025. 3. 22. 21:17·백엔드/Java + Spring
728x90
반응형

코드 중복 해결에 대한 고민과 최선의 해결책

 

코드를 보니 getUserNameByAuthentication 메소드가 여러 컨트롤러에 걸쳐 중복되고 있다. 이런 중복은 DRY(Don't Repeat Yourself) 원칙을 위반하는 전형적인 케이스다. 이런 상황에서 몇 가지 해결책을 생각해볼 수 있다.

현재 상황 분석

현재 각 컨트롤러에서 동일한 코드로 인증된 사용자의 이름을 가져오는 메소드를 반복 구현하고 있다. 이렇게 중복된 코드는:

  1. 유지보수 어려움 - 한 곳에서 수정이 필요할 때 모든 곳을 찾아 수정해야 한다
  2. 버그 발생 가능성 증가 - 일부만 수정하고 나머지는 놓칠 수 있다
  3. 코드 품질 저하 - 불필요한 코딩으로 전체 코드베이스 비대화된다

해결책들

1. 유틸리티 클래스 만들기

public class AuthenticationUtils {
    public static String getUserNameByAuthentication(Authentication authentication) throws Exception {
        CustomOAuth2User principal = (CustomOAuth2User) authentication.getPrincipal();
        if (principal == null) throw new Exception(); // TODO: Exception 만들기
        return principal.getUsername();
    }
}

이 방법은 간단하고 직관적이다. 하지만 정적 메소드를 사용한다는 특징이 있다.

2. 추상 베이스 컨트롤러 사용하기

public abstract class BaseController {
    protected String getUserNameByAuthentication(Authentication authentication) throws Exception {
        CustomOAuth2User principal = (CustomOAuth2User) authentication.getPrincipal();
        if (principal == null) throw new Exception(); // TODO: Exception 만들기
        return principal.getUsername();
    }
}

@RestController
@RequestMapping(MENTOR_URI)
@RequiredArgsConstructor
@Tag(name = "MENTOR", description = "멘토 관련 api")
public class MentorController extends BaseController {
    // 기존 코드...
}

상속을 통해 코드 재사용성을 높이는 방법이다. 다만 자바는 단일 상속만 지원하기 때문에 다른 베이스 클래스가 필요할 경우 제한이 생길 수 있다.

3. AuthenticationHelper 빈 만들기

@Component
public class AuthenticationHelper {
    public String getUserNameByAuthentication(Authentication authentication) throws Exception {
        CustomOAuth2User principal = (CustomOAuth2User) authentication.getPrincipal();
        if (principal == null) throw new CustomAuthenticationException("사용자 인증 정보가 없습니다");
        return principal.getUsername();
    }
}
@RestController
@RequestMapping(USER_URI)
@RequiredArgsConstructor
@Tag(name = "USER", description = "유저 관련 api")
public class UserController {
    private final UserService userService;
    private final UserRepository userRepository;
    private final AuthenticationHelper authHelper;
    
    // 사용예시
    public void someMethod(Authentication auth) throws Exception {
        String username = authHelper.getUserNameByAuthentication(auth);
        // ...
    }
}

이 방법은 스프링의 DI(의존성 주입)를 활용하는 방식이다.

4. 커스텀 어노테이션과 AOP 사용

복잡하지만 더 우아한 해결책으로 AOP를 활용할 수도 있다. 예를 들어 메소드에 @CurrentUsername 같은 어노테이션을 만들어서 사용하는 방식이다.

최선의 해결책

내 생각엔 3번 AuthenticationHelper 빈이 가장 좋은 방법 같다. 이유는:

  1. 유연성: 필요에 따라 다른 메소드도 추가 가능하다
  2. 스프링 철학 일치: 컴포넌트 기반 DI 패턴에 적합하다
  3. 명시적 의존성: 필요한 곳에서만 주입받아 사용한다
  4. Exception 처리: 커스텀 예외 처리도 깔끔하게 구현 가능하다

결론적으로, 이런 공통 코드는 별도 컴포넌트로 분리해서 필요한 곳에 주입받아 사용하는 것이 장기적으로 가장 유지보수하기 좋고 확장성 있는 방법이라고 생각한다.

 


현재 패턴의 문제점

// UserController
@PostMapping()
@Operation(summary = "기본정보 기입")
@ApiResponse(responseCode = "201", description = "성공!")
public ResponseEntity<ApiResponseGenerator<UserInfoResponse>> joinWithMentor(
	Authentication authentication,
	@RequestBody UserJoinRequest dto
) throws Exception {
	UserInfoResponse response = userService.saveUserInformation(
		getUserNameByAuthentication(authentication), dto);
	return ResponseEntity.created(URI.create(USER_URI))
		.body(ApiResponseGenerator.onSuccessCREATED(response));
}

// UserService
@Transactional
public UserInfoResponse saveUserInformation(String username, UserJoinRequest dto) {
	User user = findUserByUsername(username);
	user.setName(dto.getName());
	user.setPhoneNum(dto.getPhoneNum());
	return UserConverter.toResponse(userRepository.save(user));
}

 

현재 코드는 다음과 같은 패턴을 보인다:

  1. 컨트롤러에서 Authentication으로부터 username을 추출한다
  2. 그 username을 서비스 계층에 넘긴다
  3. 서비스 계층에서 다시 username으로 유저를 조회한다

이 방식은 다음과 같은 단점이 있다:

  • 반복적인 코드 패턴이 모든 엔드포인트에서 발생한다
  • 서비스 메소드마다 username 파라미터와 유저 조회 로직이 중복된다

어노테이션 기반 해결책

어노테이션 기반으로 접근하면 다음과 같은 장점이 있다:

  1. 깔끔한 컨트롤러 코드: 인증 로직이 숨겨지고 더 명확한 의도를 표현할 수 있다
  2. 반복 로직 제거: username으로 유저를 조회하는 중복 코드가 사라진다
  3. 관심사의 분리: 인증과 사용자 조회 로직을 특정 클래스에 집중시킬 수 있다
  4. 일관성: 모든 엔드포인트에서 동일한 방식으로 유저 정보를 획득한다

다음과 같이 구현할 수 있다:

// 커스텀 어노테이션 생성
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser {
}

// 어노테이션을 처리할 HandlerMethodArgumentResolver 구현
@Component
@RequiredArgsConstructor
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
    
    private final UserRepository userRepository;
    
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterType().equals(User.class) && 
               parameter.hasParameterAnnotation(CurrentUser.class);
    }
    
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                 NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
        Authentication authentication = 
            (Authentication) webRequest.getUserPrincipal();
        
        if (authentication == null) {
            throw new CustomAuthenticationException("인증 정보가 없습니다");
        }
        
        CustomOAuth2User principal = (CustomOAuth2User) authentication.getPrincipal();
        if (principal == null) {
            throw new CustomAuthenticationException("사용자 정보가 없습니다");
        }
        
        return userRepository.findByUsername(principal.getUsername())
            .orElseThrow(() -> new CustomAuthenticationException("사용자를 찾을 수 없습니다"));
    }
}

// WebMvcConfigurer에 Resolver 등록
@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
    
    private final CurrentUserArgumentResolver currentUserArgumentResolver;
    
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(currentUserArgumentResolver);
    }
}

이제 컨트롤러와 서비스는 다음과 같이 변경된다:

// UserController (수정 후)
@PostMapping()
@Operation(summary = "기본정보 기입")
@ApiResponse(responseCode = "201", description = "성공!")
public ResponseEntity<ApiResponseGenerator<UserInfoResponse>> joinWithMentor(
    @CurrentUser User user,  // 어노테이션으로 바로 User 객체 주입
    @RequestBody UserJoinRequest dto
) {
    UserInfoResponse response = userService.saveUserInformation(user, dto);
    return ResponseEntity.created(URI.create(USER_URI))
        .body(ApiResponseGenerator.onSuccessCREATED(response));
}

// UserService (수정 후)
@Transactional
public UserInfoResponse saveUserInformation(User user, UserJoinRequest dto) {
    user.setName(dto.getName());
    user.setPhoneNum(dto.getPhoneNum());
    return UserConverter.toResponse(userRepository.save(user));
}

장점 요약

  1. 코드 간소화: 컨트롤러와 서비스 모두 더 간결해진다
  2. 명확한 의도: 파라미터 자체가 도메인 객체여서 의도가 명확하다
  3. 중앙화된 로직: 인증과 사용자 조회 로직이 한 곳에 집중된다
  4. 재사용성: 모든 컨트롤러에서 일관되게 사용할 수 있다
  5. 에러 처리: 인증 및 사용자 조회 과정의 에러를 한 곳에서 처리할 수 있다

이 방식은 스프링의 철학에도 부합하며, 다수의 대규모 스프링 프로젝트에서 사용하는 방식이다. 인증 로직을 숨기고 도메인 중심적인 API를 만들 수 있어 전체적인 코드 품질이 향상된다.

728x90
반응형

'백엔드 > Java + Spring' 카테고리의 다른 글

[Java Spring] COGO 개발일지 - Spring Security에서 JWT 인증 처리 방식 비교 (JwtAuthenticationFilter)  (0) 2025.04.01
[백엔드] Blog - 모든 엔티티 생성 시간, 업데이트 시간 설정하기 @CreatedDate, @LastModifiedDate, BaseEntity  (1) 2024.09.24
[백엔드] Blog - 전역 예외 커스텀 처리하기 @RestControllerAdvice, @ExceptionHandler  (1) 2024.09.24
[백엔드] Blog - 실행 오류 정리 (application.yml, SDK, test)  (0) 2024.09.19
[백엔드] 회원탈퇴(Soft delete) 및 회원복구  (1) 2024.09.08
'백엔드/Java + Spring' 카테고리의 다른 글
  • [Java Spring] COGO 개발일지 - Spring Security에서 JWT 인증 처리 방식 비교 (JwtAuthenticationFilter)
  • [백엔드] Blog - 모든 엔티티 생성 시간, 업데이트 시간 설정하기 @CreatedDate, @LastModifiedDate, BaseEntity
  • [백엔드] Blog - 전역 예외 커스텀 처리하기 @RestControllerAdvice, @ExceptionHandler
  • [백엔드] Blog - 실행 오류 정리 (application.yml, SDK, test)
dev_ares
dev_ares
대학에서 컴퓨터공학을 전공하고 있는 학생입니다.
    반응형
    250x250
  • dev_ares
    노트
    dev_ares
  • 전체
    오늘
    어제
    • 분류 전체보기 (188)
      • IT 트랜드 (2)
      • 백엔드 (18)
        • Java + Spring (8)
        • Kotlin + Spring (5)
        • 백엔드 (5)
      • 프론트엔드 (1)
        • React (1)
      • 대외활동 (17)
        • 42서울 (17)
      • 백준 (6)
        • Java (2)
        • C++ (3)
      • 전공 (121)
        • 객체지향프로그래밍 (17)
        • 자료구조 (23)
        • 리눅스시스템관리 (16)
        • 컴퓨터구조 (25)
        • 네트워크 (25)
        • 데이터베이스 (15)
        • 기타 전공 (0)
      • 프로그래밍 언어 (18)
        • Java (5)
        • Swift (4)
        • C++ (1)
        • Kotlin (8)
      • 기타 (4)
      • 공군 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    컴퓨터구조
    데이터패스
    자료구조
    메모리 계층 구조
    컴공 포트폴리오
    코틀린
    백준
    오블완
    리눅스
    상속
    티스토리챌린지
    명령어
    사설 문제
    반복자
    컴퓨터 구조 및 설계
    C++
    추가 문제
    42서울
    자바
    단일 사이클
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.0
dev_ares
[Java Spring] COGO 개발일지 - 코드 중복 해결에 대한 고민 해결책 (DI, AOP, 어노테이션)
상단으로

티스토리툴바