본문 바로가기

카테고리 없음

객체지향적으로 중복 검증 로직 제거하기 (feat. @Embedded)

문제

사용자인 User는 로그인ID를 가지고있다. 현재는 이 값들을 String 타입으로 표현하고 있지만, 이렇게되면 다음과 같이 동일한 검증 로직이 중복되는 문제가 발생한다.

 

현재 로그인 ID를 검증하는 기능은 여러 API에서 이루어진다.

// 1. 회원가입 API
@PostMapping("/signup")
public ResponseEntity<Void> signup(
    // SignupRequest 내부에서 loginId에 대한 검증이 이루어짐
		@RequestBody @Valid SignupRequest signupRequest,
		HttpServletRequest httpServletRequest
) {
	Long savedUserId = userService.signup(signupRequest);

	return ResponseEntity
			.created(URI.create(httpServletRequest.getRequestURI() + "/" + savedUserId))
			.build();
}

public record SignupRequest(
		@NotBlank
		@Length(min = 5, max = 20)
		String loginId,
    // ...
) {}

// 2. 회원가입 시 로그인 ID의 중복을 검증하는 API
@PostMapping("/signup/check-duplicate-id/{loginId}")
public ResponseEntity<Void> checkDuplicateId(
		// 마찬가지로 같은 조건의 검증이 이루어짐
		@PathVariable @NotBlank @Length(min = 5, max = 20) String loginId
) {
	userService.validateDuplicateLoginId(loginId);

	return ResponseEntity.noContent().build();
}

// 3. 로그인 API
@PostMapping("/login")
public ResponseEntity<Void> login(
		@RequestBody @Valid LoginRequest loginRequest,
		HttpServletRequest httpServletRequest
) {
	UserAuthDTO userAuth = userService.login(loginRequest);

	HttpSession session = httpServletRequest.getSession();
	UserSessionUtil.setLoginUserAuth(session, userAuth);

	return ResponseEntity.noContent().build();
}

public record LoginRequest(
		@NotBlank
		@Length(min = 5, max = 20)
		String loginId,
		// ...
) {}

위 코드에서 보이듯이, 회원가입 API, 로그인 API에서 각각 같은 검증 코드가 중복적으로 작성된다. 현재는 3개의 API지만, 만약 10개의 API에서 로그인 ID를 요청으로 받는다면 10개의 중복 검증 코드가 생기고, 로그인 ID에 대한 검증 조건이 바뀌게 된다면 또 10개의 API에서 코드를 수정해줘야 한다. 또한 실수로 한 두개의 API에서는 다른 검증 조건으로 코드를 작성할 수도 있다.

 

해결

로그인 ID를 String이 아닌 객체로 관리한다.

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Embeddable
public class LoginId {

    private static final int MIN_ID_LENGTH = 5;
    private static final int MAX_ID_LENGTH = 20;
    private static final String INVALID_ID_LENGTH_MESSAGE = "로그인 ID 길이 검증에 실패했습니다.";

    @NotBlank
    @Length(min = MIN_ID_LENGTH, max = MAX_ID_LENGTH)
    @Column(name = "login_id", unique = true, length = 15, nullable = false)
    private String idValue;

    public LoginId(String idValue) {
        Assert.isTrue(validatedLoginIdLength(idValue), INVALID_ID_LENGTH_MESSAGE);

        this.idValue = idValue;
    }

    private boolean validatedLoginIdLength(String idValue) {
        return idValue.length() >= MIN_ID_LENGTH && idValue.length() <= MAX_ID_LENGTH;
    }
}

LoginId 클래스를 생성한다. 이 클래스는 로그인 ID에 대한 상태와 행위를 가지고 있다.

 

  • @Embeddable : User 클래스 내부에서 엔티티의 값으로 사용되기 때문에 해당 어노테이션을 적용한다.
  • @NoArgsConstructor(access = AccessLevel.PROTECTED) : 기본 생성자는 사용할 일이 없기 때문에 접근을 최대한 막는다.
  • @NotBlank, @Length : 입력값 검증 어노테이션을 적용한다. 이렇게 LoginId와 관련된 입력값 검증 로직을 해당 클래스에서 처리함으로써 String으로 관리할 때 여러 컨트롤러에서 중복되던 로직이 하나의 클래스에서 관리된다.
  • validatedLoginLength() : LoginId 객체가 입력값을 바인딩 하지 않고 코드를 통해 생성되는 경우 값 검증을 하기 위한 메소드

 

결과

// 1. 회원가입 API
@PostMapping("/login")
public ResponseEntity<Void> login(
		@RequestBody @Valid LoginRequest loginRequest,
		HttpServletRequest httpServletRequest
) { ... }

// 2. 회원가입 시 로그인 ID의 중복을 검증하는 API
@PostMapping("/signup")
public ResponseEntity<Void> signup(
		@RequestBody @Valid SignupRequest signupRequest,
		HttpServletRequest httpServletRequest
) { ... }

public record SignupRequest(
		@Valid
		LoginId loginId
)

// 3. 로그인 API
@PostMapping("/signup/check-duplicate-id/{loginId}")
	public ResponseEntity<Void> checkDuplicateId(
			@PathVariable @Valid LoginId loginId
	) { ...}

이전과 달리 이제 LoginId를 요청으로 받는 API 마다 검증 어노테이션을 적용해 주는게 아니라 LoginId 내부에 검증 로직이 작성되어 있으니 @Valid 어노테이션만 작성해주면 된다.

 

배운점

종합적으로 String 대신 LoginId와 같이 객체로 관리하면 다음과 같은 이점이 있다.

  • 중복 로직 제거
    • LoginId와 관련된 로직이 LoginId 클래스 내부에 정의된다.
  • 명확한 의미 전달
    • 애매한 변수나 파라미터 명이 아닌 클래스 명으로 어떤 책임을 가지는지 명확하게 알 수 있다.
  • User 객체의 책임 분산 및 LoginId 객체의 응집도 증가
    • 만약 로그인 ID의 길이를 반환하는 기능이 있다고 하자. 로그인 ID를 String으로 관리한다면 User나 서비스 레이어에서 해당 기능을 구현할 수 있다. User에서 구현한다면 User가 가지는 책임이 많아진다. 서비스 레이어에서 구현한다면 로그인 ID의 길이를 반환하는 기능이 여러 서비스 레이어에서 필요한 경우, 서비스 레이어마다 중복적으로 코드를 작성해줘야 한다. 이를 LoginId 객체 내부에서 관리함으로써 User 객체의 책임을 분산하고, LoginId 객체의 응집도를 증가시킬 수 있다.

 

마지막으로 조영호님의 <오브젝트 2장: 객체지향 프로그래밍>에 다음과 같은 구절이 있다.

금액을 구현하기 위해 Long 타입을 쓸 수 있지만, Money라는 객체를 만들어서 사용할 수도 있다. 기본 타입 대신 객체를 만들어서 사용하면 저장하는 값이 금액과 관련돼 있다는 의미를 전달할 수 있다. 또한 금액과 관련된 로직이 여러 곳에 중복되어 구현되는 것을 막을 수 있다. 따라서 의미를 좀 더 명시적이고 분명하게 표현할 수 있다면 객체를 사용해서 해당 개념을 구현하라 그 개념이 비록 하나의 인스턴스 변수만 포함하더라도 개념을 명시적으로 표현하는 것이 전체적인 설계의 명확성과 유연성을 높이는 첫걸음이다.

즉, 객체를 사용하여 명시적으로 개념을 표현하거나 중복 구현을 제거함으로써 설계의 명확성과 유연성을 높이자.

 

 

 

📜 References

https://product.kyobobook.co.kr/detail/S000001766367

https://cheese10yun.github.io/spring-jpa-best-04/

https://cheese10yun.github.io/spring-jpa-best-07/