인프런 영문 브랜드 로고
인프런 영문 브랜드 로고

인프런 커뮤니티 질문&답변

yhwjd님의 프로필 이미지
yhwjd

작성한 질문수

Java/Spring 주니어 개발자를 위한 오답노트

어디까지 추상화 해야 하는가?

DB 엔티티와 도메인 분리

해결된 질문

작성

·

3.9K

3

어디까지 추상화 해야 하는가? 강의에서 2분 17초에

toDomain()으로 바꿔서 반환을 했는데

이렇게하면 JPA 엔티티가 아니라서 변경감지를 사용하지 못하는거 아닌가요?

이러한 방식으로 작성한다면 업데이트가 필요할 때는 어떤식으로 처리하나요?

답변 2

2

김우근님의 프로필 이미지
김우근
지식공유자

안녕하세요. 답변이 늦어 죄송합니다. 다른 일에 집중하고 있어서 근 한 달이 넘어서야 답변을 달게 되네요. 양해 부탁드립니다. 일단 질문이 2가지인 것 같아서 정리 후 답변드립니다.

질문 1. JPA 변경 감지를 못 사용하게 되는 문제

말씀하신 대로 좋은 설계를 쫓다 보면 점점 JPA의 꿀 같은 기능들을 사용하기 어려워집니다. 그리고 그렇게 되다보면 종장에는 ‘이럴 거면 ORM을 왜 사용하지?’라는 생각이 들기까지 합니다. 이는 당연합니다. 왜냐하면 좋은 설계의 목표가 어떤 특정 라이브러리에 종속되는 상황을 피하기 위함이기 때문입니다. 그렇기 때문에 변경 감지를 사용하지 못하게 되는 부분은 저는 어쩔 수 없다고 생각합니다.

이 질문에 대한 대답은 https://www.inflearn.com/questions/978220 여기서 답한 질문과 맥이 비슷한 것 같아 링크 첨부합니다.

질문 2. 변경 감지를 사용하지 않고 영속성 객체를 업데이트하는 방법

업데이트가 필요하다면 아래와 같은 방법들이 있을 수 있습니다.

  1. Repository에 UpdateDto 같은 것을 만들어 보낼 수 있을 겁니다.

  2. Repository에 변경된 도메인 모델을 전달해 Repository가 변경을 일일이 확인하고 이를 저장하도록 코드를 만들 수도 있을 겁니다.

아래는 테스트 강의 때 만들어 둔 레포지토리인데, 혹시나 참고 되실까 올려드립니다. (여기서는 2번 방법을 사용했습니다.) https://github.com/kok202/test-code-with-architecture/tree/test/practice-part-2

커뮤니티에 올라온 질문들을 읽어보니까 질문자님과 비슷한 생각을 하시는 분들이 많더라고요. 이론과 실무 사이에서 고민하게 되는 것은 자연스러운 현상이라 생각합니다. 그래서 개발이 재밌는 것 같기도 하고요. 오히려 정답이 정해져 있지 않기 때문에 의미 있는 것 같습니다. (만약 정답이 정해져 있는 문제였다면 정말로 GPT가 개발자를 모두 대체했을지도 모릅니다. 😂)

답변이 조금이라도 도움 되셨길 바랍니다.

-1

안녕하세요, 인프런 AI 인턴이에요. DB 엔티티와 도메인을 분리하고 추상화의 범위에 대해 궁금하신 것 같아요.

JPA 엔티티와 도메인 클래스를 분리하는 것은 일반적으로 좋은 아키텍처 설계 방법 중 하나입니다. 이렇게 분리하면 도메인 클래스는 비즈니스 로직을 담고, JPA 엔티티는 데이터베이스 패러다임에 맞춘 관리를 담당하게 됩니다. 이렇게 분리함으로써 독립적인 관심사로 서로를 영향시키지 않고 유지보수성을 높일 수 있습니다.

2분 17초에서 toDomain() 메서드가 호출되었을 때, 해당 메서드는 JPA 엔티티를 도메인 객체로 변환하기 위한 로직을 담고 있을 겁니다. 이렇게 변환한 도메인 객체를 사용하여 비즈니스 로직을 수행하고, 변경이 필요한 상황에서는 도메인 객체를 수정한 후, JPA 엔티티로 다시 변환하여 업데이트 작업을 수행할 수 있습니다.

만약 변경된 도메인 객체를 JPA 엔티티로 변환하는 과정에서 변경 감지가 제대로 이루어지지 않는다면, 변경된 필드를 수동으로 업데이트해야 할 수도 있습니다. 이때는 JPA의 EntityManager를 사용하여 업데이트 작업을 직접 수행할 수 있습니다.

추상화는 해당 엔티티나 도메인의 특징과 범위에 따라 달라질 수 있습니다. 일반적으로는 특정 DB 시스템에 종속되지 않고 여러 DB에 대응할 수 있는 범용적인 추상화를 고려하는 것이 좋습니다. 하지만 적절한 추상화 수준을 설정하기 위해서는 프로젝트의 요구사항과 복잡도, 유지보수성 등을 고려해야 합니다.

더 자세한 내용이 필요하시다면, 실제 프로젝트나 강의에 대한 구체적인 내용을 공유해주시면 더욱 도움이 될 것 같아요. 언제든지 질문해주세요!

    public void writeIncome(IncomeWriteDto incomeWriteDto) {
        // Repository에서 Entity를 찾고, Domain으로 변환한다.
        Member findMember = memberRepository.findById(incomeWriteDto.getMid()).orElseThrow(
                () -> new CustomApiException("사용자가 없습니다.")
        );
        MemberDomain memberDomain = new MemberDomain(findMember);

        // 잔여금을 계산한다.
        memberDomain.calculateRest(memberDomain.getRest() + incomeWriteDto.getIncomeMoney());


        // 년, 월을 계산한다.
        String month = incomeWriteDto.getMonth();
        String year = month.substring(0,4);

        Income income = Income.builder()
                            .incomeMoney(incomeWriteDto.getIncomeMoney())
                            .incomeReason(incomeWriteDto.getIncomeReason())
                            .year(year)
                            .month(month)
                            .member(findMember)
                            .build();

        incomeRepository.save(income);
    }

 

위 코드의 경우
영속성 객체와 도메인 객체를 분리했고,
도메인 객체 내부에 매서드를 만들어
비즈니스 로직을 구현했습니다.

이 때, calculateRest() 매서드 후
영속성 객체의 값이 변경되어야 하는데,

어떻게 영속성 객체의 값을 변경할 지
잘 모르겠습니다.

findMember.setRest()를 사용하자니
DDD에 어긋나는 것 같고,

아니면 calculateRest() 파라미터에
findMember를 전달해주고
이 내부에서 처리를 해야하는 걸까요?

김우근님의 프로필 이미지
김우근
지식공유자

AI에 코드와 관련된 질문 주셨기에 미약하게나마 아래와 같이 피드백 드립니다. 저라면 아래와 같이 변경할 듯합니다.

public void writeIncome(IncomeWriteDto incomeWriteDto) {
    MemberDomain memberDomain = memberRepository.getById(incomeWriteDto.getMid()).
    memberDomain.addRest(incomeWriteDto.getIncomeMoney());
    memberRepository.update(memberDomain);
    Income income = Income.builder()
        .incomeMoney(incomeWriteDto.getIncomeMoney())
        .incomeReason(incomeWriteDto.getIncomeReason())
        .year(incomeWriteDto.getYear())
        .month(incomeWriteDto.getMonth())
        .member(findMember)
        .build();
    incomeRepository.save(income);
}

member가 JPA엔티티라고 가정했습니다.

  1. new MemberDomain(member) 처럼 작성한다면 MemberDomain 이 영속성 객체에 의존하게 됩니다. 생성자가 영속성 객체를 알게되니까요. 그러면 도메인 레이어는 영속성 객체에 의존하지 않는다는 설계의 목적을 달성할 수 없게 됩니다.

  2. getById와 findById를 구분해서 사용하여 서비스 코드에 findById.orElseThrow가 계속 나오는 상황을 방지할 것 같습니다. 이에 대한 책임(orElseThrow)은 Repository로 옮길 것 같습니다.

  3. memberDomain.calculateRest의 의도가 무엇인지 정확히 와닿지 않습니다. 추측하건데 이 메서드의 호출 결과로 도메인 내부 값이 변경되고 JPA의 변경 감지로 값이 변경되길 원하셨던 것 같습니다. 이런 의도라고 생각하고 그렇다면 관련해서 아래와 같은 답변을 드립니다.

    1. memberDomain.calculateRest(incomeWriteDto.getIncomeMoney())가 되면 될 것 같습니다. 자기 자신의 값을 외부에서 가져다 계산해서 넣어줄 이유가 없습니다. 내부적으로 주입받은 파라미터 값과 본인이 들고 있는 값을 더해 변경하도록 하면 됩니다.

    2. 영속성 객체와 도메인 모델을 분리하기로 결정한 순간 JPA의 변경 감지 기능은 더이상 못 쓰게 됩니다. (제가 아는 선에서 답변드리는 겁니다. 분명 더 나은 해법을 갖고 계신 분이 계실 수 있습니다.)

    3. 변경 감지로 코드를 짜는 코드가 좋은 코드인지 저는 잘 모르겠습니다. 그렇게 생각하는 이유는 의도가 안보이기 때문입니다. 저는 코드를 보면서 calculateRest의 의도가 무엇인지 알 수 없었습니다. 질문자님이 작성해 주신 코드의 의도를 읽은 후에야 ‘이 코드로 인해 member 모델의 값이 변경되길 원하는 거구나’라는 것을 알았습니다. 심지어 아직도 이게 의도하신 것이 맞는지 모릅니다. 그렇게 추측만 할 뿐입니다. 이러한 코드가 누적되면 서비스의 의도가 보이지 않게 된다고 생각합니다. 다른 사람이 짠 서비스 호출 결과 데이터 변경으로 인해 영속성 데이터의 값이 변경되지 않을지를 걱정하며 코드를 짜야 합니다. 그렇게 되면 결국 그러한 확신을 얻기 위해 하위 호출되는 메서드가 값을 변경하고 있지는 않은지 모두 까봐야 합니다. 영속성 데이터의 변경을 원하신다면 그 코드가 차라리 보이는 게 낫다고 생각합니다.

  4. 년 월을 계산하는 책임은 incomeWriteDto에게 넘길 것 같습니다. IncomeWriteDto가 아래와 같은 메서드를 갖고 있으면 됩니다. (month라는 변수가 yyyy-mm 처럼 오는 것 같은데, 만약 그렇다면 변수 이름이 잘못된 듯 하네요)

    public String getYear() {
        return month.substring(0,4);
    }
    

답변이 도움되셨길 바랍니다. :)

yhwjd님의 프로필 이미지
yhwjd

작성한 질문수

질문하기