본문 바로가기
독서/자바,스프링 개발자를 위한 실용주의 프로그래밍

10. 도메인 & 11. 알아두면 유용한 스프링 활용법

by Thinking 2024. 11. 13.

소프트웨어 공학에서 말하는 '도메인'은 애플리케이션이 해결하고자 하는 문제 영역을 의미합니다. 백엔드 개발자 입장에서 도메인은 어떻게 바라보는 것이 좋은지, 애플리케이션을 개발한다는 말이 왜 도메인을 개발한다는 말과 같은지, 애플리케이션의 본질은 스프링이나 JPA가 아니라 도메인이다라는 말이 왜 존재하는지 알아보죠.

 

10.1 소프트웨어 개발의 시작

사업가 입장에서 소프트웨어 시스템을 만들게 되는 계기를 먼저 이해해보죠. 일반적으로 비즈니스는 '소프트웨어를 만들어야겠다'라는 전체로 시작되지 않기 때문이죠. 사용자가 겪는 문제를 해결하는 것이 비즈니스입니다. IT 사업가란 사람들이 겪는 문제를 분석, 솔루션 고안, 이를 소프트웨어로 구현하는 사람들이라고 볼 수 있습니다.

 

린(lean) : '군더더기 없는'이라는 뜻으로, 린 생산이란 생산 공정을 군더더기 없이 처리하겠다는 의미이고, '린 방식의 업무 스타일'은 군더더기 없이 업무를 수행하겠다는 의미입니다. '군더더기 없음'이란, 불필요한 지출을 최소화하겠다는 뜻입니다. 린 방식의 업무 스타일은 아래와 같습니다.

1) 사용자의 문제 상황을 인식한다.

2) 문제 상황에 따라 어떤 솔루션을 제공하면 좋은 반응을 얻을 것이라고 가설을 세운다.

3) 가설이 맞다면 결과가 어떤 지표로 반영될 것이라고 가정한다.

4) 가설을 검증할 수 있는 가장 빠른 방법을 생각하고 이를 실험한다.

5) 사용자와 지속적으로 소통하면서 가설의 방향성을 지속적으로 조정, 확장한다.

 

린 업무의 방식에서는 사용자가 겪는 문제를 강조합니다. 그리고 사용자의 문제를 해결할 수 있는 해결책을 만들어야 함을 강조합니다. 즉, 사용자들이 겪는 문제 영역이 바로 도메인입니다. 문제 영역이 곧 비즈니스 영역이므로 도메인은 비즈니스 영역을 의미하기도 합니다. 도메인은 문제 영역이자 비즈니스 영역입니다.

 

그렇다면 개발자의 역할은 무엇인가요? 단순히 요구사항에 맞는 애플리케이션을 개발해 주는 것이 아닌, 고객이 겪는 문제 상황을 소프트웨어로 해결해 주는 사람입니다. 즉, 개발자는 도메인을 분석하고, 고객이 겪는 문제를 인지하고, 이에 맞는 도메인 솔루션을 개발해 줄 수 있어야 합니다.

 

우리가 개발해야 하는 것은 '도메인' 어플리케이션입니다. 도메인이 갖고 있는 특성과 요구사항을 이해하지 못한 채로 개발된 소프트웨어는 무용지물이 될 확률이 높습니다. 이러한 배경으로 만들어진 것이 도메인 주도 설계입니다. '도메인이 주도해서 설계해야 한다'라는 의미가 담겨있습니다. 좋은 개발자는 도메인을 분석하고, 사용자들이 겪는 문제를 인지해 소프트웨어적인 해결책을 제시할 수 있습니다.

 

 

10.2 애플리케이션의 본질

'도메인을 먼저 생각해야 합니다' 도메인을 분석하고 정리하는 것이 먼저여야 합니다. 시스템의 설계, 패턴, 세부 구현은 분석한 도메인을 바탕으로 선택해야 합니다. 우리의 역할은 도메인 애플리케이션을 만드는 것입니다. 프로젝트 설계는 도메인을 설명할 수 있어야 합니다. 

 

 

10.3 도메인 모델과 영속성 객체

도메인 모델과 영속성 객체는 구분해야 하나요? 다시 말해 '도메인 모델과 JPA 엔티티는 구분해서 개발해야 하나요?'에 관한 내용입니다.

 

구분하기 전략 : 역할에 따라 도메인 모델과 영속성 객체를 나눕니다. 이는 '계정'이라는 모델이 있을 때 도메인 모델로서 존재하는 Account 클래스와 데이터베이스 영속화를 담당하는 영속성 객체인 AccountJpaEntity 클래스를 분리해서 관리하겠다는 의미입니다.

 

통합하기 전략 : 도메인 모델과 영속성 객체를 하나의 클래스로 관리합니다. 즉, Account 클래스 하나에 두 객체의 역할이 들어갈 수 있게 합니다. 이 전략을 사용하는 프로젝트에서 Account 클래스는 도메인 모델이면서 동시에 영속성 객체입니다. 프로젝트가 JPA를 사용하고 있다면, @Entity 애너테이션을 Account 클래스에 등록해서 사용하고 있음을 의미합니다.

 

먼저 아래를 가정하고 어떤 전략이 나은지 확인해보죠.

1) 계정(account)이라는 도메인이 있습니다.

2) 계정은 닉네임을 변경할 수 있습니다.

3) 계정은 데이터베이스에 저장되고 불러올 수도 있어야 합니다.

 

 

10.3.1 통합하기 전략

구분하기 전략에서는 유사한 모델을 똑같이 두 번 만들어야 합니다. 그리고 영속성 객체를 도메인 모델로 매핑하는 메서드도 추가해야 합니다. 통합하기 전략에서는 하나의 클래스만 잘 관리하면 되고, 구분하기 전략에 비해 개발 속도가 빠릅니다. 더불어 JPA의 역할이 ORM이라는 점을 상기했을 때 통합하기 전략을 선택하는 것은 매우 자연스러운 일입니다.

 

즉, 시작부터가 관계형 데이터를 객체지향에서 말하는 Object에 그대로 매핑해서 사용하기 위해 만들어진 라이브러리인 것입니다. ORM의 목적을 고려했을 때 도메인 모델과 영속성 객체를 분리하려는 시도는 잘못된 것일 수 있습니다. 다만 이 전략은 클래스의 책임이 눈에 잘 들어오지 않는다는 단점이 있습니다. 도메인 모델에 영속성 객체와 관련된 코드가 들어 있으면 개발자는 데이터베이스 위주의 사고를 하기 쉽습니다.

 

예로 요구사항 변경이나 도메인 모델이 확장돼야 할 때 프로젝트에서 통합하기 전략을 사용하고 있다면, 도메인을 어떻게 변경할지보다 데이터베이스 스키마 변경이나 마이그레이션 걱정을 먼저 하게 될 것입니다. 그래서 도메인 모델이 커질수록 모델을 관리하기가 어려워지죠.

 

 

10.3.1 구분하기 전략

도메인 모델을 위한 클래스와 영속성 객체를 위한 클래스를 분리합니다. 누군가 '이 전략을 사용할거면 ORM을 왜 사용하냐?'라고 반문할 것입니다. 데이터를 불러와서 영속성 객체로 변환하고, 이를 도메인 모델에 그대로 매핑할 것이라면 구태어 ORM을 사용할 이유가 없기 때문입니다. 그런데 그것이 구분하기 전략이 추구하는 바입니다. 애초에 구분하기 전략은 도메인이 ORM 같은 특정 라이브러리에 의존하지 않게 하기 위해 만들어진 전략입니다. 그렇게 해서 애플리케이션의 데이터베이스 접근 라이브러리를 MyBatis, JdbcTemplate으로도 교체가 가능하게 만들려고 했던 것입니다.

 

그리고 구분하기 전략에서는 애플리케이션이 관계형 데이터베이스에도 의존하지 않습니다. 클래스를 구분했던 이유는 이처럼 도메인의 책임과 데이터 영속의 책임을 구분해 유연함을 얻기 위해서였습니다. 통합하기 전략은 단일 책임 원칙을 위반합니다. 도메인 모델로서의 역할, 영속성 객체로서의 역할을 통합했기 때문입니다.

 

다만 작성해야 하는 코드가 많아진다는 점은 확실한 단점입니다. 이렇게 되면 ORM이 갖는 혜택을 누리기 어렵죠.

 

통합하기 전략

1) 도메인과 영속성 객체를 통합합니다.

2) 작성해야 하는 코드의 양이 줄어듭니다.

3) 도메인 영속성 라이브러리에 강결합됩니다.

 

구분하기 전략

1) 도메인과 영속성 객체를 분리합니다.

2) 작성해야 하는 코드의 양이 늘어납니다.

3) 도메인과 영속성 라이브러리가 분리됩니다.

 

 

11. 알아두면 유용한 스프링 활용법

2부를 마무리하기에 앞서 스프링과 관련된 코딩 테크닉과 개념을 소개하겠습니다. 

 

 

11.1 타입 기반 주입

스프링에서 @Autowired 애너테이션을 이용한 의존성 주입은 타입을 기반으로 동작합니다. 의존성 주입이 필요할 경우 스프링 컨테이너는 타입을 기반으로 빈을 찾는다는 말입니다. @Autowired 애너테이션은 일치하는 타입의 빈을 찾아 이를 주입하고, 만약 해당하는 빈을 찾지 못한다면 NoSuchBeanDefinitionException 에러를 던집니다.

 

스프링이 스프링 컨테이너를 초기화하는 과정에서 동작이 모호해질 수 있는 지점이 하나 있습니다. 주입하려는 타입이 인터페이스와 같이 추상 타입이라고 가정할 때, 프로그램에 해당 추상 타입을 상속하거나 구현하려는 빈이 여러 개일 수 있죠. 그럼 스프링은 어떻게 동작하고, 어떤 빈을 주입할지 어떻게 선정할까요?

 

이처럼 어떤 빈을 주입해야 할지 선택할 수 없는 상황이 생길 경우 스프링은 NoUniqueBeanDefinitionException 에러를 던지죠. 이를 극복하기 위해 @Qualifier나 @Primary 같은 애너테이션이 제공되는데, 두 애너테이션 모두 주입할 수 있는 빈이 여러 개일 때 사용할 수 있는 애너테이션입니다. @Qualifier 애너테이션을 이용하면 @Qualifier("emailNotificationChannel") 처럼 주입하려는 빈을 지정할 수 있습니다. @Primary 애너테이션을 이용하면 타입이 일치하는 빈이 여러 개일 때 특정 빈을 가장 우선해서 주입하게 할 수 있습니다.

 

내용을 확장해서 추상 타입이 존재하고, 이를 상속하거나 구현하려는 빈이 여러 개라고 가정하고, 주입받으려는 변수가 List <추상타입>이라면 어떻게 할까요? List 타입의 멤버 변수를 주입하려 할 때는 타입과 일치하는 모든 스프링 빈을 찾아 List의 요소로 넣어주는 방식으로 처리합니다. 스프링의 타입 기반 주입을 활용하면 SOLID에서 말하는 OCP를 프레임워크 수준에서도 적용할 수 있게 됩니다.

 

 

11.2 자가 호출

자가 호출은 어떤 객체가 메서드를 처리하는 와중에 자신이 갖고 있는 다른 메서드를 호출하는 상황을 의미합니다. 특히 스프링의 빈 메서드에서 자가 호출이 일어나면 이야기다 다릅니다. 특히 자가 호출되는 메서드에 AOP 애너테이션이 지정돼 있을 경우 문제가 됩니다. 자가 호출이 발생하면 호출되는 메서드에 적용된 AOP 애너테이션이 동작하지 않습니다. 예로 @Transactional 애너테이션의 부가 기능이 실행되지 않습니다.

 

이것은 스프링의 AOP가 프록시를 기반으로 동작하기 때문에 발생하는 현상입니다. 스프링 AOP는 프록시 객체를 만들어 추가 동작을 삽입하는 방식으로 AOP의 부가 기능이 동작하게 합니다. 메서드에 지정된 AOP 애너테이션이 수행되려면 반드시 이 프록시 객체를 통해 메서드가 실행돼야 합니다.

 

이러한 탓에 메서드를 자가 호출하는 상황에서 프록시의 부가 기능이 실행되지 못합니다. 프록시를 거치지 않고 클래스에 정의된 메서드를 곧바로 호출하기 때문이죠. 스프링이 AOP를 위한 프록시를 만들고, 프록시 객체를 생성하고, 타겟 객체 대신 프록시를 실행하는 등의 모든 동작을 자동으로 처리하기 때문입니다. AOP를 적용해야 하는 컴포넌트가 있다면, 컨테이너에 컴포넌트 빈을 생성하는 동시에 프록시 객체도 함께 생성합니다.

 

이렇게 만들어진 프록시 객체는 원본 빈 객체의 메서드 호출을 감싸는 형태로 대신 호출됩니다. 그렇게 해서 AOP에서 말하는 횡단 관심사를 타겟 객체의 메서드가 호출되기 전이나 후에 적용합니다. 즉, AOP 애너테이션이 지정된 메서드의 호출은 타겟 객체의 메서드를 직접 호출하는 것이 아니라 프록시 객체를 통해 간접 호출되는 것입니다.

 

@Transactional 애너테이션을 예로 살펴보죠. 해당 애너테이션을 사용하는 스프링 컴포넌트는 컴포넌트 빈 객체가 생성되면서 이에 대응하는 프록시 빈 객체도 함께 만들어집니다. 이 프록시 빈은 원본 객체에서 @Transactional 애너테이션이 적용된 메서드가 호출되는 것을 감시합니다. 그리고 해당 메서드가 호출되는 순간 해당 메서드 호출이 try-catch-finally로 감싸진 채로 실행되도록 합니다. 이를 통해 트랜잭션의 시작, 커밋, 롤백과 같은 작업들이 수행될 수 있게 합니다.