[프로젝트] boolean is prefix 문제

공부하는 감자·2024년 2월 22일
0

F-Lab 프로젝트

목록 보기
10/11

들어가는 말

계속해서 Spring Rest API 문서를 만들던 중 boolean형의 변수에서 문제가 생겼다.

먼저 근본적인 문제와 여러 해결 방안들을 정리하고, 적용한 방안에 대해 적어보았다.

그 이후 수정하면서 오류가 났던 순으로, mapstruct에서 생긴 문제와 테스트 코드에서 발생한 문제를 해결한 것을 적어보았다.

boolean is prefix 오류

boolean 타입 변수에 is prefix(접두사)를 붙였을 때, mapstruct와 API 문서 작성을 위한 테스트 코드에서 문제가 발생했다.

원인

mapstruct 연동과 편의를 위해 lombok 라이브러리를 사용했다.

이때, lombok에서 제공하는 @Getter 어노테이션은 boolean 형일 경우 앞에 get 이 아닌 is 를 자동으로 붙여준다고 한다.

그래서 isDefault 라고 쓰면 해당 객체의 변수를 가져올 때 default 로 인식해버리는 문제가 있었다.

해결 방안

케이스 별로 보기 전에 공통적으로 적용할 수 있는 해결 방안부터 정리했다.

  • is 접두사를 사용하지 않기
  • Getter 메서드를 직접 생성
  • Wrapper Class 사용
  • lombok 설정 변경

1. is 접두사를 사용하지 않기

가장 간단한 해결 방법으로, 여러 사이트에서 이 방법을 권고하고 있다.

private boolean active;

// lombok이 자동 생성해준다.
public boolean isActive() {
  return active;
}

내 경우에는 boolean 타입의 이름이 isDefault 여서 default 가 변수 명이 되므로 적용할 수 없었다.

달리 적합한 이름을 찾아 변경하면 되지만, 이미 스펙을 boolean형은 앞에 ‘is’를 붙이기로 결정해서 이 방법은 넘어갔다.

2. Getter 메서드를 직접 생성

lombok이 자동 생성하는 getter 메서드가 아니라, 직접 메서드를 정의하는 방법이다.

private boolean isActive;

// 직접 생성
public boolean getIsActive() {
  return isActive;
}

나는 lombok에 의존하는 mapstruct를 사용하는데, 이 경우 @Getter 를 필수로 적용해야 해서 이 방법도 적용할 수 없었다.

3. Wrapper Class를 사용

Primitive type인 boolean 이 아니라, Wrapper Class인 Boolean 을 사용하면 변수 이름을 그대로 사용할 수 있다.

Boolean 은 Refrence type이어서 그대로 사용하는 듯하다.

private Boolean isActive;

// lombok이 자동 생성해준다.
public boolean getIsActive() {
  return isActive;
}

다만, 이러면 모든 boolean 변수를 Boolean 객체로 사용해야 하는데, 다음 문제들이 우려되었다.

  • 메모리 사용량 증가
    • Boolean은 boolean 변수보다 더 많은 메모리를 사용하므로, 모든 변수를 Boolean으로 사용하면 메모리 사용량이 증가할 수 있다.
    • 실제로 유의미한 차이를 보이는지는 검증해봐야 할 거 같다.
  • 성능 저하
    • Boolean은 boolean 변수보다 연산 속도가 느리므로, 모든 변수를 Boolean으로 사용하면 프로그램 성능이 저하될 수 있다.
    • 이 부분도 실제로 유의미한 차이를 보이는지 검증해봐야 할 거 같다.
  • Boolean은 Null 값을 가질 수 있다.
    • boolean과 같은 Primitive type은 null 허용이 안되는데, Boolean은 null이 들어갈 수 있으므로 NullPointException이 발생할 수 있다.
    • null이 허용되기 때문에 코드의 의미가 혼동될 수 있다.
  • 불필요한 자동 박싱/언박싱
    • Boolean 객체를 사용하면 자동 박싱/언박싱 과정이 발생한다.
    • 이는 불필요한 오버헤드를 발생시킬 수 있다.
  • API 호환성 문제
    • 일부 API는 기본형 boolean 변수만 지원한다.

따라서 이 방법을 적용할 땐 request/response DTO만 Boolean으로 적어놓고, 도메인 계층으로 넘길 때는 boolean으로 사용하게끔 했었다.

  • request DTO에 Boolean으로 선언
  • mapstruct에서 @Mapping 을 이용해 boolean과 Boolean을 매핑

4. lombok 설정 변경

lombok 설정: @Getter and @Setter

  • 프로젝트 루트 폴더에 lombok.config 설정 파일을

Lombok 설정 파일에서 lombok.getter.noIsPrefix 속성의 값을 변경한다.

lombok.getter.noIsPrefix@Getter 어노테이션으로 생성된 getter 메서드 이름에서 is 접두사를 생략하도록 설정한다.

lombok.getter.noIsPrefix = true

단, lombok 설정을 변경하면 아래와 같은 문제가 발생할 수 있다. (AI Gemini 이용)

  • 코드 가독성 및 유지 관리
    • 기본 lombok 설정과 다른 방식으로 코드가 생성되므로, 코드를 읽고 이해하기 어렵게 만들 수 있다.
    • 다른 개발자가 코드를 이해하고 수정하기 어려울 수 있다.
  • 버그 발생 가능성
    • lombok 설정을 변경하면 예상치 못한 버그가 발생할 가능성이 증가한다.
  • IDE 지원
    • 일부 IDE는 lombok 설정을 완벽하게 지원하지 않을 수 있다.
  • 팀 내 코드 스타일 불일치
    • 팀 내 개발자가 서로 다른 lombok 설정을 사용하면 코드 스타일이 달라져서 코드 가독성 및 유지 관리에 영향을 미칠 수 있다.

✨ 적용한 방법: lombok 설정 파일

내가 하고 싶은 것은 다음과 같았다.

  • 작업량이 적을 것
    • Mapper처럼 어노테이션을 일일이 달아주는 작업이 없을 것
  • 타입을 바꾸지 않을 것

그리고 최종적으로 lombok 설정 파일을 삽입하기로 결정했다.

  1. 프로젝트 루트에 lombok.config 파일을 추가한다.
  2. 파일에 lombok.getter.noIsPrefix = true 를 적는다.
  3. 프로젝트 빌드 후 테스트!

💡 아래에선 오류와 오류를 해결하기 위해 찾아봤던 과정(lombok 설정 파일 적용 방법 제외)을 정리했습니다.

1. MapStruct 오류

오류

MapStruct를 이용해 mapper를 자동으로 생성해주고 있었는데, 이때 매핑이 제대로 되지 않는 문제가 있었다.

해결 방안

찾아본 결과 해결 방안은 아래와 같았다.

  • 수동으로 매핑 어노테이션 적용
  • 커스텀 NamingStrategy를 작성한 후 SPI를 등록
    • 이건 하단 참고 사이트를 보길 바란다.
    • MapStruct가 PropertyName을 판단하는 로직을 직접 커스터마이징하는 방식으로, 구현 방법도 사용도 복잡하다.
  • Wrapper Class 사용

수동으로 매핑 어노테이션 적용

is 접두사가 붙은 필드를 수동으로 매핑하도록 @Mapping 을 사용한다.

  • 이때, source는 is 접두사를 제거하고, target은 is 접두사를 붙인 상태로 사용한다.
    • 인터넷에선 source와 target 모두 isXXX로 적었는데, 그렇게 하면 오류가 발생한다.
  • 다만, boolean을 사용하는 모든 곳에 일일이 매핑해야 한다는 번거로움이 있다.
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface MemberDeliveryAddressMapper {
    MemberDeliveryAddressMapper INSTANCE = Mappers.getMapper(MemberDeliveryAddressMapper.class);

    @Mapping(source = "default", target = "isDefault")
    DeliveryAddress toDeliveryAddress(MemberDeliveryAddressRegisterRequest deliveryAddressRegisterRequest);

    @Mapping(source = "default", target = "isDefault")
    MemberDeliveryAddressRegisterResponse toMemberDeliveryAddressRegisterResponse(DeliveryAddress deliveryAddress);
}

Wrapper Class 사용

앞서 설명한 것처럼, boolean 대신 Boolean을 사용하는 방식이다.

  • @Mapping 으로 일일이 설정하지 않아도 된다.
  • 다만, Boolean 객체를 사용하면서 문제가 발생할 수 있다.
    • 가장 우려했던 건 Null이 들어갈 수 있다는 것이다.

2. MvcMock 오류

오류

분명 매핑은 잘 된 것 같은데, 테스트 코드를 돌리는데 계속해서 오류가 났다.

isBoolean 오류1.PNG

빨간색은 테스트 코드의 예상 결과가 다르다는 것이니 위에 오류 메시지를 다시 읽어봤다.

JSON parse error: Unrecognized field "default" (class com.flab.funding.infrastructure.adapters.input.data.request.MemberDeliveryAddressRegisterRequest), not marked as ignorable]

즉, JSON parse 오류가 났다는 건데 문제는 아래 코드에 있었다.

  • MemberDeliveryAddressRegisterRequest 객체 생성
  • objectMapper.writeValueAsString 로 JSON으로 변환
public class MemberDeliveryAddressRestAdapterTest {

    ...

    @Test
    void registerDeliveryAddress() throws Exception {
        // given
        MemberDeliveryAddressRegisterRequest request = MemberDeliveryAddressRegisterRequest.builder()
                .userKey("1L")
                .isDefault(true)
                .zipCode("01234")
                .address("서울특별시 강서구")
                .addressDetail("OO 아파트 xxx동 xxxx호")
                .recipientName("홍길동")
                .recipientPhone("010-1111-2222")
                .build();

        //when
        //then
        this.mockMvc.perform(post("/deliveryAddresses")
                        .content(objectMapper.writeValueAsString(request))
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andDo(document("{class-name}/{method-name}",
                        requestFields(
                                ..
                        ),
                        responseFields(
                                ...
                        )));
    }
}

바로 여기서, JSON으로 변환할 때 isDefaultdefault 로 변경되어서 문제였다.

해결 방안

  • JsonProperty 어노테이션 사용
  • Wrapper Class 사용
    • 이 부분은 위에서 설명했으니 넘어가도록 하겠다.

JsonProperty 어노테이션 사용

JsonProperty는 Java 객체와 JSON 간의 매핑을 위한 Jackson 라이브러리에서 제공하는 어노테이션이다.

Java 객체의 필드와 JSON 객체의 속성을 연결하는 역할을 하며, 아래와 같이 Json 속성 명을 직접 지정할 수 있다.

@JsonProperty("isDefault")
private Boolean isDefault;
  • 하지만 이 방법의 경우 isDefault와 default 두 개가 생겨버리는 문제가 있다.
  • Spring Rest API는 요청 파라미터와 응답 파라미터를 모두 문서화하기 때문에, 이 방식을 사용할 수 없다.

Wrapper Class 사용

boolean을 Boolean으로 변경한다. 이때, Boolean을 사용하는 것이 마음이 들지 않았던 터라 한 가지 생각을 했다.

  • request/response는 Boolean으로 선언
  • MapStruct에서 @Mapping 으로 Boolean과 boolean을 매핑 (이름은 동일하게 선언했다)
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface MemberDeliveryAddressMapper {
    MemberDeliveryAddressMapper INSTANCE = Mappers.getMapper(MemberDeliveryAddressMapper.class);

		// 도메인으로 변환할 때는 둘 다 isDefault
    @Mapping(source = "isDefault", target = "isDefault")
    DeliveryAddress toDeliveryAddress(MemberDeliveryAddressRegisterRequest deliveryAddressRegisterRequest);

    @Mapping(source = "default", target = "isDefault")
    MemberDeliveryAddressRegisterResponse toMemberDeliveryAddressRegisterResponse(DeliveryAddress deliveryAddress);
}

즉 외부에서 값을 받아와 JSON으로 변환할 때만 Boolean을 사용했다.

이러면 도메인 계층으로 넘길 때 값이 null이면 false로 들어갈 거고, 모든 변수를 Boolean으로 사용하는 것이 아니니 문제도 어느 정도 해결되는 게 아닐까?

접는 말

이유는 찾았는데 문제를 어떻게 해결해야 할 지가 고민돼서, 몇 주에 걸쳐 계속 조사하면서 수정했다.

모든 boolean를 Boolean으로 썼다가, 도메인 계층에선 boolean으로 쓰게 수정했다가, 다시 lombok 설정을 발견해서 최종 수정까지 거쳤다.

이랬다가 저랬다가 한 커밋 내역을 보면 좀 심란해진다. 실무에서는 is 를 사용하지 않는 편이 좋을까?

이건 계속 고민될 거 같다.

Reference

참고 사이트

MapStruct가 is 접두사를 가진 boolean 필드를 매핑하지 못하는 문제

https://github.com/mapstruct/mapstruct/issues/1943

[Error] Response JSON에서 Boolean의 is가 생략되는 문제

RequestBody Annotation 사용 시 boolean 변수 바인딩 에러

Educative Answers - Trusted Answers to Developer Questions

@Getter and @Setter

profile
책을 읽거나 강의를 들으며 공부한 내용을 정리합니다. 가끔 개발하는데 있었던 이슈도 올립니다.

0개의 댓글

관련 채용 정보