필독 개발자 온보딩 가이드

'필독 개발자 온보딩 가이드' 책에 대한 감상과 의견 메모입니다.

Table of Contents
Cover

감상

(내돈내산 후기입니다)

많지 않은 분량으로 실무 개발자의 폭넓은 업무를 다루면서 결코 얄팍하지 않은 지식을 전달하는 책입니다. 협업, 설계, 구현, 테스트, 문서화, 긴급 대응, 경력 관리 등 다양한 분야에 대한 알찬 조언을 책 한권에 눌러 담았고 더 많은 분량을 학습하고 싶은 독자를 위한 참고자료들도 매 장마다 소개하고 있습니다.

이 책은 신입 개발자를 대상으로 한 책으로 홍보되고 있지만 경력이 많은 개발자가 읽을만한 가치도 충분합니다. 저도 과거의 경험을 돌아보면서 생각을 정리할 수 있었습니다. 스스로 절실하게 얻은 교훈이 이 책에도 적혀 있는 보면 반가웠고 깊은 공감이 되는 문장들도 많이 만날수 있었습니다.

예를 들면, Dark launching이나 Feature toggle 같이 제 경험에서 유용했던 기법이 개발 실무의 액기스를 전하는 이 책에서도 소개되니 반갑고 안심이 되었습니다. 긴급 대응을 위한 당번(on call) 제도와 같이 더 개선을 해야 할 요소도 독서 후에 더 크게 느껴지기도 했습니다. 전에는 썼지만 근래에는 활용하지 않았던 Liquibase를 통한 DB 스키마 관리도 다음 프로젝트에서는 다시 시도해봐야겠다는 생각도 듭니다.

유용하다고 느낀 지침과 조언을 새겨두고 실천하기 위해 여러 번 읽어야겠다는 생각을 하면서 마지막 장을 덮었습니다.

의견 메모

책을 보다가 의견을 메모한 부분을 옮겨봅니다.

p102

예외는 일찍 던지고 최대한 나중에 처리하자.

최대한 나중에 처리하는 것보다는 적절한 추상화 레이어에서 처리하자는 것이 좋다고 생각한다. 예를 들면 사용자의 ID로 조회했을 때 DB 조회가 실패했을 때 생기는 `EmptyResultDataAccessException`을 어플리케이션 콜스택의 마지막에서 처리하기보다는 비지니스 로직을 다루는 레이어에서 `UserNotFoundException`과 같이 비지니스 맥락의 의미가 부여된 예외로 바꾸는 것이 좋을 때도 있다.

p204

기능 브랜치 기반 개발은 트렁크의 코드가 사용자에게 릴르스하기엔 너무 불안정해서 트렁크를 안정화시키는 동안 개발자가 기능 개발을 수행할 수 없는 경우에 사용하는 방법이다. 고객이 각자 다른 버전의 소프트웨어를 사용하는 경우에 보편적으로 기능 브랜치 기반 개발 전략을 채택한다. 서비스 지향 시스템(service-oriented systems)은 일반적으로 트렁크 기반 개발 전략을 채택한다.

가장 보편적인 기능 브랜치 전략은 2010년 빈센트 드리센이 소개한 깃플로(Gitflow)라고 부르는 전략이다.

Git-flow는 필요에 따라 팀 브랜치 정책의 종착역은 될수 있지만 시작점으로 권장하고 싶지는 않다. 자세한 의견은 Git-Flow에 대해서 다시 생각해보기라는 글로 따로 정리했다.

p212

버전 제어 시스템을 릴리스 리포지토리처럼 사용할 수는 있지만 원래 용도라고 보기 어렵다. 버전 제어 시스템에서는 검색이나 배포 관련 기능을 별로 제공하지 않는다. 대규모 배포를 위해 만들어진 시스템이 아니므로 그런 상황에서는 문제를 일으킬 수도 있다. 버전 제어 시스템 머신과 동일한 머신이 개발자 체아크웃, 도구로부터의 요청, 배포 요청을 모두 담당하면 프로덕션 배포에 영향을 미칠 수 있다.

Node.js나 Go언어의 오픈소스 생태계에서는 GitHub.com을 릴리스 리포지토리처럼 쓰는 관행이 굳어졌다. 하지만 사내에서라면 이를 분리하는 것이 좋다는 저자의 의견에 동의한다. 버전 관리 시스템은 GitHub Enterprise를 쓰더라도 릴리스 저장소는 Nexus나 Artifactory를 쓰는 식이다. 분리하면 두 시스템을 각각 업그레이드하기에도 유리하다.

p213

릴리스는 최대한 자주 수행하자. 릴리스 주기가 늘어지 거짓된 안정감을 심어줄 수 있다. 즉 릴리스 사이의 주지가 길면 변경사항을 테스트할 충분한 시간이 있는 것처럼 느껴지기 때문이다. 실제로 릴리스 주기를 짧게 가져가면 더 안정적인 소프트웨어를 구현할 수 있어 버그가 발견됐을 때 더 쉽게 처리할 수 있다. 매 주기마다 릴리스되는 변경사항의 수가 더 적으므로 각 릴리스의 위험도도 낮아진다.

공감한다. 1~2주에 한번 릴리스하는 조직과 한달에 한번 릴리스하는 조직을 비교했을 때 후자의 릴리스가 훨씬 힘겹고 위험한 일이 되는 현상을 몇 번 목격했다. 테스트와 모니터링의 자동화 수준이 높아져야 릴리스의 비용이 적어져서 더 자주 릴리스하게 될 수 있기도하다.

p212

배포를 원자적으로 만드는 가장 쉬운 방법은 기존에 설치된 소프트웨어를 덮어쓰는 것이 아니라 다른 경로에 소프트웨어를 설치하는 것이다. 일단 패키지가 설치된 다음에는 단축 아이콘이나 심볼릭 링크를 이용해 교체하면 된다.

(의견) 많이 쓰던 기법이다. 사내 배포 시스템에서 디폴트로 제공이 되기도한다. 이동욱 님이 발표한 우아한스프링배치 발표에서도 이 기법을 응용한 무중단 배포를 소개하고 있다. 배포된 최신 버전 파일로 심볼릭 링크를 교체하고 (ln -s -f v2.jar app.jar) readlink 명령어로 그 파일을 실행하는(java -jar ${readlink ./app.jar}) 이용하는 방식이다.

p219

한 번에 모조리 새 코드로 전환하는 일은 위험하다. 테스트를 아무리 많이 해도 버그 발생 가능성을 없앨 수는 없으며 한 번에 모든 사용자에게 코드를 롤아웃하면 모두가 동시에 문제를 겪을 수 있다. 따라서 변경 사항을 점진적으로 롤아웃하고 시스템 상태 지표를 모니터링 하는 편이 좋다.

규모가 있는 시스템을 변경하는 개발자라면 절실하게 읽어야할 문장이다.

p311

데이터베이스 스키마 마이그레이션 도구인 Liquibase 등을 소개함.

데이터베이스와 애플리케이션 수명주기를 결합해서는 안 된다. 애플리케이션 배포 과정에서 스키마를 마이그레이션하는 것은 위험하다.

의도한 바은 아니지만 JPA의 스키마 관리 기능이 운영환경에서 실행되어서 발생하는 장애를 종종 전해듣는다. 스프링 부트의 spring.jpa.hibernate.ddl-auto 같은 옵션도 Local PC에 DB가 따로 설치되어 있거나 Embeded DB를 쓸때만 활성화해야 한다고 생각한다. 공용 개발 DB에서부터는 Liquibase와 같은 스키마 관리 도구를 활용하는 것이 좋다. 공용 개발 DB라도 여러 개가 있는 것이 다양한 테스트를 하는데 유리하고, 개발 DB가 여러개가 되면 스키마 관리 도구의 필요성이 높아진다. Oracle 등 개발자가 여러 인스턴스를 설치하는데 제약이 있는 DB를 오랫동안 써 온 개발자는 이런 도구의 필요성을 느끼거나 적용할 수 있는 기회를 얻기가 어려울 수도 있다.

운영DB는 DBA가 따로 SQL파일을 실행해서 스키마를 반영하는 회사라도 Liquibase는 충분히 사용 가능하다. Liquibase는 오프라인 지원 기능이 있어서 실제 DB에 스키마 변경을 실행하지 않고 DDL을 파일로 뽑아낼수도 있다. 예를 들어 스키마 버전 1.0.0-RC1과 1.0.0-RC2 버전 사이에 변경된 컬럼을 반영하는 DDL을 담은 .sql파일을 뽑을 때는 아래 명령어로 가능하다.

rm target/release.csv
mvn liquibase:updateSQL -Dliquibase.url='offline:mysql?changeLogFile=target/release.csv' -Dliquibase.toTag=1.0.0-RC1
mvn liquibase:updateSQL -Dliquibase.url='offline:mysql?changeLogFile=target/release.csv' -Dliquibase.toTag=1.0.0-RC2

추천자료

이 책에서는 각 장의 끝무렵에 ’레벨업을 위한 읽을 거리’로 해당 장에서 다룬 주제를 더 깊이 있게 학습하는데 도움이 되는 자료를 추천했습니다. 여러 번 언급되는 구글의 SRE 관련 책들은 https://sre.google/books/ 에서 무료로 열람도 가능합니다.

표기된 제목이 여러가지 이유로 달라진 책들은 다음과 같습니다.

2장 역량을 높이는 의식적 노력

4장 운영 환경을 고려한 코드 작성: 개발환경과 프로덕션 환경은 엄연히 다르다.

5장 피할 수 없는 코드 의존성의 관리: 복잡한 프로그램을 짜봐야 비로서 깨닫는 의존성이 진실

7장 올바로 주고받는 코드 리뷰: 원만한 팀 협업과 높은 코드 품질을 목표로

9장 긴급대응 온콜 업무

10장 견고한 소프트웨어를 위한 기술 설계 절차

11장 소프트웨어 수명주기를 고려한 진화하는 아키텍처 구현

12장 효율적인 협업을 위한 애자일 문화

13장 관리자, 팀장, 상사와 함께 일하기

Git-Flow에 대해서 다시 생각해보기

Git의 브랜치 전략으로 Git-flow가 가장 유명하고 자주 언급됩니다. 그런데 이 전략은 복잡도와 실행하는 난이도가 높은 편입니다. Git-flow를 쓰고 있다고 주장하는 조직에서도 창시자 Vincent가 제안한 원래의 프로세스를 그대로 쓰는 경우는 드물다고 느껴집니다.

Git-flow가 특히 인터넷 서비스 개발에서 보편적으로 권장할만한 전략일지는 저는 의문입니다. 브랜치 전략은 복잡하게 시작해서 단순하게 줄여나가기 보다는 단순하게 시작해서 필요에 따라 복잡도를 늘여가는 편이 시행착오가 적다고 생각합니다. 여러 버전을 동시에 운영하고 백포트 패치를 해야하는 설류션 개발에서는 Git-flow가 적합한 조직이 있을법도 합니다. 즉, Git-flow는 필요에 따라 종착역은 될수 있지만 시작점으로 권장하고 싶은 정책은 아닙니다.

Git-flow가 그 이름 때문에 필요 이상으로 권장되고 있다고 저는 생각합니다. Git이 지금보다 대중화되지 않았을 때 제안된 모델이고 그 당시 다른 유명한 모델이 없겠기에 Git-flow라고 불리지 않았을까 추정하기도합니다. 만약 창시자의 이름을 딴 Vincent-flow정도의 이름이였다면 많은 사람들이 따르려고 생각하지 않았을지도 모릅니다. Git-flow가 유용해보인다면 그 이름이 Vincent-flow라도 마찬가지일지를 한번 돌아봐도 이름에 과도하게 끌린건 아닌지 돌아보는데 도움이 될듯합니다.

뱅크 샐러드에서는 Git-flow보다 더 단순한 전략으로 리뷰-배포 프로세스를 개선했다는 사례도 있습니다.

먼저 Git-flow를 활용한다면 하나의 기능을 배포하는 데 있어서 무려 5번의 branch switching이 필요하고, 6번의 Pull Request와 이에 따른 6번의 Code Review가 필요했습니다. 이 말은 다르게 말하면 코드 오너가 6번이나 코드를 리뷰하고 승인을 해줘야 한다는 것이죠. 이렇게 복잡한 프로세스는 자연스럽게 배포를 귀찮은 존재로 만듭니다. 이로 인해 간단한 수정 사항의 경우 develop branch에 merge 한 뒤 배포하지 않는 일이 종종 발생하기 시작했습니다. 이런 배포되지 않은 변경 사항이 쌓이면서 나중에 필요할 때 한 번에 너무 많은 변경사항을 포함한, 부담스러운 배포를 진행해야만 했습니다. 배포 시 변경 사항이 많을수록 장애가 발생할 수 있는 확률은 당연히 증가하기 때문에 개발자에게는 배포는 무서운 존재가 되었습니다.

저는 GitLab-flow정도면 많은 사람이 협업하는 인터넷 서비스를 개발하는 브랜치 전략으로도 괜찮다고 생각합니다. 이를 참고해서 필요한 나름의 정책을 추가로 정의할 수도 있겠습니다.

Git-flow의 창시자 Vincent Driessen는 그의 아티클에 10년간의 회고를 덧붙였습니다. 거기서 지속적인 배포를 하는 프로젝트에서는 GitHub-flow와 같은 더 단순한 모델을 권장한다는 언급을 했습니다.

If your team is doing continuous delivery of software, I would suggest to adopt a much simpler workflow (like GitHub flow) instead of trying to shoehorn git-flow into your team.

귀사의 팀이 지속적으로 소프트웨어를 제공하고 있다면, 귀사의 팀에 git-flow를 도입하는 대신 훨씬 간단한 워크플로우(GitHub flow와 같은)를 채택하는 것을 제안합니다.

(번역은 파파고에게 맡겼습니다.)

많은 조직에서는 특히 운영 환경에서는 지속적인 배포(continuous delivery)와는 먼 프로세스를 택하고 있을 수도 있습니다. 그럼에도 개발과 테스트 환경에서라도 자주 활발히 통합되어 테스트되고 리뷰되는걸 추구한다면 Vincent의 회고를 더 깊이 새겨둘만합니다.

아래 문장으로 Vincent의 10년만의 회고는 끝이 납니다.

To conclude, always remember that panaceas don’t exist. Consider your own context. Don’t be hating. Decide for yourself.

결론적으로, 만병통치약은 존재하지 않는다는 것을 항상 기억하세요. 자신만의 맥락을 고려하세요. 미워하지 말고, 스스로 결정하세요.

Jackson으로 파싱한 JSON 속성값을 생성자로 전달하기

Jackson으로 JSON을 파싱한 속성값을 객체의 생성자로 전달할 수 있는 여러가지 방법을 정리했습니다.

  • 변경이력

    • 2023.06.25 : Spring Boot Gradle plugin에서 -parameter 옵션을 넣어주는 소스로의 링크 변경

    • 2022.05.07 : 예제 프로젝트를 JDK17로 변경

1. Jackson에서 (no Creators, like default construct, exist) 에러 메시지

파싱하고자하는 JSON
{
    "accessDateTime": "2019-10-10T11:14:16Z",
    "ip": "175.242.91.54",
    "username": "benelog"
}

위와 같은 JSON을 파생해서 아래와 같이 setter가 없는 객체에 집어 넣고 싶은 경우가 있습니다.

파싱한 결과를 넣을 클래스
public class AccessLog {
    private final Instant accessDateTime;
    private final String ip;
    private final String username;

    public AccessLog(Instant accessDateTime, String ip, String username) {
        this.accessDateTime = accessDateTime;
        this.ip = ip;
        this.username = username;
    }

    public Instant getAccessDateTime() {
        return accessDateTime;
    }

    public String getIp() {
        return ip;
    }

    public String getUsername() {
        return username;
    }
}

Jackson 라이브러리로 JSON을 파싱하는 테스트 코드를 아래처럼 작성했습니다.

테스트 코드
class ConstructorPropertiesTest {
    @Test
    void parse() throws JsonProcessingException {
        var json = """
            {
            "accessDateTime": "2019-10-10T11:14:16Z",
            "ip": "175.242.91.54",
            "username": "benelog"
            }
            """;

        var objectMapper = new ObjectMapper()
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .registerModule(new JavaTimeModule());

        AccessLog accessLog = objectMapper.readValue(json, AccessLog.class);

        then(accessLog.getAccessDateTime()).isEqualTo("2019-10-10T11:14:16Z");
        then(accessLog.getIp()).isEqualTo("175.242.91.54");
        then(accessLog.getUsername()).isEqualTo("benelog");;
    }
}

위의 코드를 실행하면 다음의 Exception이 떨어집니다. (no Creators, like default construct, exist) 이 핵심적인 메시지입니다.

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `net.benelog.jackson.ConstructorPropertiesTest$AccessLog` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (String)"{
"accessDateTime": "2019-10-10T11:14:16Z",
"ip": "175.242.91.54",
"username": "benelog"
}
"; line: 2, column: 1]

	at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
	at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1592)
	at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1058)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1297)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:326)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:159)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4218)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3214)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3182)

JSON을 파싱한 결과를 전달할 적절한 생성자를 찾지 못했을 때 발생하는 에러입니다. 이 문제를 해결하는 방법을 정리합니다.

2. 생성자로 JSON 속성값을 전달하는 방법들

2.1. @JsonCreator

Jackson에서 제공하는 @JsonCreator, @JsonProperty 를 값을 전달할 생성자와 메서드 파라미터에 붙입니다.

AccessLog의 생성자에 @JsonCreator 선언
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

public class AccessLog {

    // 멤버 변수 선언 생략

    @JsonCreator
    public AccessLog(
        @JsonProperty("accessDateTime") Instant accessDateTime,
        @JsonProperty("ip") String ip,
        @JsonProperty("username") String username) {

        this.accessDateTime = accessDateTime;
        this.ip = ip;
        this.username = username;
    }

    // getter 생략
}
  • 장점

    • JSON의 속성명과 객체의 멤버변수명이 다를 때도 자연스럽게 활용할 수 있습니다.

    • 생성자가 에러 개 일때 Jackson에서 사용할 생성자를 명시적으로 지정할 수 있습니다.

  • 단점

    • Jackson에 의존적인 방법입니다.

      • Jar파일로 배포하는 클래스 안에서 이 방법을 사용하려면 Jackson에 대한 의존성이 추가됩니다.

      • JSON 파싱 라이브러리를 교체한다면 전체 클래스를 수정해야 합니다.

2.2. @ConstructorProperties

JDK 1.6부터 제공되었던 @java.beans.ConstructorProperties 은 생성자의 파라미터 이름을 지정하는 표준적인 방법입니다. 이를 활용하면 생성자의 파라미터 이름을 Reflection API를 통해서 알 수 있습니다. Jackson은 2.7.0버전부터 @ConstructorProperties 를 인지합니다. ( https://github.com/fasterxml/jackson-databind/issues/905 참조)

생성자에 @ConstructorProperties 으로 파라미터의 이름을 지정하면, Jackson에서는 동일한 이름의 JSON솔성값을 생성자로 넘겨줍니다.

AccessLog의 생성자에 `@ConstructorProperties`로 속성명 지정
import java.beans.ConstructorProperties;

public class AccessLog {

    // 멤버 변수 선언 생략

    @ConstructorProperties({"accessDateTime", "ip", "username"})
    public AccessLog(Instant accessDateTime, String ip, String username) {
        this.accessDateTime = accessDateTime;
        this.ip = ip;
        this.username = username;
    }

    // getter 생략
}

Lombok을 활용한다면 이 과정을 더 편하게 할 수 있습니다. lombok.config 를 다음과 같은 선언을 하면 Lombok에서 만드는 생성자에서 @ConstructorProperties 를 자동으로 넣어줍니다.

lombok.config 설정
lombok.anyConstructor.addConstructorProperties=true

@Builder, @AllArgsConstructor 와 같은 애노테이션을 클래스에 붙이면 Lombok에서는 자동으로 생성자를 만들어줍니다. 이를 통해 JSON 파싱한 값을 넣을 클래스를 더 단순하게 만들 수 있습니다.

Lombok을 이용한 AccessLog 클래스 선언
@Builder
@Getter
@ToString
public class AccessLog {
    private final Instant accessDateTime;
    private final String ip;
    private final String username;
}

참고로 Lombok v1.16.20 전까지는 디폴트로 @ConstructorProperties 을 넣어줬었다고 합니다. 이 이후 버전부터는 디폴트가 아니므로 lombok.config 에 명시적인 선언이 필요합니다. ( https://multifrontgarden.tistory.com/222 참조 )

@ConstructorProperties 를 직접 쓸 때의 장단점은 다음과 같다고 생각합니다.

  • 장점

    • @JsonCreator + @JsonProperties 보다는 코딩량이 조금 적습니다.

    • Jackson에 의존적이지 않습니다.

      • JSON을 파싱한 값이 들어가는 클래스를 jar 파일로 배포할 때 Jackson의 의존관계가 딸려들어가지 않습니다.

      • 같은 방식을 지원하는 다른 JSON 파싱 라이브러리로 교체할 때 코드 변경이 없습니다.

  • 단점

    • JSON의 속성명과 생성자의 실제 파라미터 명이 다른 경우에는 사용하는 것이 부자연스럽습니다.

만약 아래와 같이 @ConstructorProperties 에서는 "ip_address"로 지정한 속성이 실제 파라미터이름이 String ip 경우라면, 코드로는 잘 동작하지만 애노테이션의 원래 의도하는 어긋난 것이 아닌가 하는 생각이 들었습니다.

    @ConstructorProperties({"accessDateTime", "ip_address", "username"})
    public AccessLog(Instant accessDateTime, String ip, String username) {
        this.accessDateTime = accessDateTime;
        this.ip = ip;
        this.username = username;
    }

@ConstructorProperties + Lombok 은 코드량이 적다는 장점이 있지만 멤버 변수의 이름이 JSON 속성명과 일치해야 한다는 단점도 있습니다. jar 파일로 배포하는 클래스라면 Lombok에 대한 의존성이 부담스러울수도 있습니다.

2.3. ParameterNameModule 활용

앞의 예제들을 보면 @JsonProperty("ip") 와 같이 지정하는 속성의 이름과 생성자의 파라미터의 이름이 동일합니다. String ip 와 같이 생성자의 파라미터의 이름을 바로 가지고 올 수 있다면 일일히 속성명을 지정하지 않을 수 있겠다는 생각이 들만합니다.

그런데 JDK 8이 나오기 전까지는 Reflection만으로는 파라미터 이름을 가지고 올 수 없었고, ASM과 같은 바이트코드 조작 라이브러리를 이용해서 디버깅을 위한 정보를 이용해야만 가능했습니다. ( https://stackoverflow.com/questions/2729580/how-to-get-the-parameter-names-of-an-objects-constructors-reflection#2729907 참조) 그래서 앞서 소개한 @java.beans.ConstructorProperties 와 같은 애노테이션도 활용되었습니다.

JDK8 이상에서는 컴파일을 할 때 -parameters 라는 옵션을 붙이면 Reflection API로 파라미터 정보를 가지고 올수 있도록 컴파일된 클래스에 정보를 덧붙여 줍니다. Gradle을 쓰고 있다면 아래와 같이 설정할 수 있습니다.

build.gradle 안의 컴파일 옵션에 추가
tasks.withType(JavaCompile).each {
    it.options.compilerArgs.add('-parameters')
}

IDE 안에서도 컴파일 옵션을 신경써줘야합니다.

IntelliJ에서는 Settings > Build, Execution, Development > Build Tools > Gradle 에서 Build and Run using: 옵션을 확인해 봅니다.

intellij-settings-gradle.png

이 옵션값이 Gradle(Default)`로 되어 있다면, `build.gradle 의 컴파일 옵션이 그대로 쓰입니다. 만약 그 값이 IntelliJ IDEA 로 되어 있다면 IntelliJ 안에서의 Java 컴파일 옵션도 동일하게 맞춰 줘야합니다.

Settings > Build, Execution, Development > Compiler > Java Compiler 메뉴에서 Addtional command line parameters 옵션에 -parameters 을 적어줍니다. 옵션을 바꾼 후에는 전체 프로젝트를 리빌드합니다. ( Build > Rebuild Project )

intellij-settings-java-compiler.png

Jackson의 ParameterNameModule 을 쓰기 위해서는 다음과 같이 의존성을 추가해야합니다.

ParameterNameModule 의존성 추가
    implementation 'com.fasterxml.jackson.module:jackson-module-parameter-names:2.10.3'

ObjectMapper 선언에서는 registerModule() 메서드로 ParameterNamesModule 을 추가합니다.

ObjectMapper에 ParameterNamesModule 추가
    var objectMapper = new ObjectMapper()
        .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
        .registerModule(new JavaTimeModule())
        .registerModule(new ParameterNamesModule());

이렇게 하면 생성자에 특별한 애너테이션을 붙이지 않아도 Jackson은 JSON의 속성을 생성자에게 전달됩니다.

Spring Boot에서는 ParameterNamesModule 을 편하게 쓸 수 있도록 아래와 같은 기본 설정이 제공됩니다.

  • Spring Boot Gradle Plugin에서 Java 컴파일의 -parameters 옵션이 자동 추가됩니다.

  • spring-boot-starter-web 에서 이미 jackson-module-parameter-names 에 대한 의존성이 추가되어 있습니다.

  • 디폴트로 등록되는 ObjectMapper bean에는 ParameterNamesModule 이 이미 추가되어 있습니다.

    • JacksonAutoConfiguration.java#L108 참조

    • RestTeamplteBuilderRestTemplate 을 생성한다면 디폴트 등록된 ObjectMapper 을 참조하는 MappingJackson2HttpMessageConverterRestTemplate 에 주입됩니다.

ParameterNamesModule 은 Lombok에서 자동으로 만든 생성자도 잘 인식합니다. lombok.config 에 추가 설정을 하지 않아도 된다는 점이 @ConstructorProperties 를 쓸 때와의 차이점입니다.

이 방식의 장단점은

  • 장점

    • 코드가 짧습니다.

    • Jackson에 대한 의존성이 없습니다.

  • 단점

    • 생성자의 파라미터명과 JSON 속성의 이름이 반드시 일치해야 합니다.

      • 생성자의 파라미터 이름이 JSON파싱에 쓰인다는것을 의식하지 않는다면, 파라미터 명을 잘 모르고 고쳐서 JSON 파싱이 안되게 하는 부작용이 쓰일수 있습니다.

    • 컴파일 옵션을 의식하지 않으면 특정 개발자의 IDE에서는 의도대로 동작하지 않을수 있습니다.

    • 생성자가 여러 개 일때는 @JsonCreator 와 같은 다른 방식과 병행해서 써야 합니다.

3. 예제 소스 저장소

예제는 https://github.com/benelog/jackson-experiment 에 올려두었습니다.

이 예제는 JDK 17을 써서 작성했습니다. Text blocks 문법을 썼기 때문에 JDK 15이상이 필요합니다. 이 문법이 'Preview’로 들어간 JDK 13,14에서는 컴파일 옵션으로 '--enable-preview' 을 넣어야합니다.