"인프런의 스프링 핵심원리 - 기본편 강의를 듣고 작성한 글 입니다."
스프링과 객체지향 원리를 사용하기에 앞서, 우선 순수 자바 코드만으로 회원 도메인과 주문, 할인 도메인을 설계하며 웹 서비스를 만들어보고, 이를 통해 어떤 문제점을 발견할 수 있으며, 스프링과 객체지향 원리를 통해 문제를 어떻게 해결할 수 있는지 알아보자.
그동안 이론에서 이해가 되지 않던 부분을 코드를 작성해보며 이해해보자.
앞서 말한듯이 이번 예제에서는 스프링을 사용하지 않고 순수 자바 코드만으로 웹 서비스를 만들어 볼 것이다.
비즈니스 요구사항과 설계
회원, 주문, 할인 정책 등 3가지 요구사항이 있다.
* 회원
- 회원을 가입하고 조회할 수 있다.
- 회원은 일반과 VIP, 두 가지 등급이 있다.
- 회원 데이터는 자체 DB 를 구축할 수 있고, 외부 시스템과 연동할 수도 있다.(미확정)
* 주문과 할인 정책
- 회원은 상품을 주문할 수 있다.
- 회원 등급에 따라 할인 정책을 적용할 수 있다.
- 할인 정책은 모든 VIP 는 1000 원을 할인해주는 고정 금액 할인을 적용해달라(나중에 변경될 수 있다.)
- 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 못 정했고, 오픈 직전 까지 고민을 미루고 싶다.
최악의 경우 할인을 적용하지 않을 수도 있다.(미확정)
요구사항을 보면 회원 데이터, 할인 정책과 같은 부분은 지금 결정하기 어려운 부분이다.
그렇다고 이런 정책이 결정될 때까지 개발을 무기한 기다릴수도 없다.
그런데 우리는 앞에서 배운 객체지향 설계 방법이 있지 않은가?
인터페이스를 만들고 구현체를 언제든지 갈아끼울 수 있도록 설계하면 된다.
* 참고 : 프로젝트 환경설정을 편리하게 하기위해 스프링 부트를 사용한다. 지금은 스프링 없는 순수한 자바 코드로만 개발을 진행한다는 점을 꼭 기억하자.
회원 도메인 설계
* 회원 도메인 협력 관계
위의 회원 도메인 요구사항을 참조하여 협력 관계를 구성하면 아래와 같다.
- 클라이언트가 회원 서비스를 호출한다.
- 회원 서비스는 회원 가입과 회원 조회 등 2가지 기능을 제공한다.
- 회원 저장소를 별도로 만든다. 즉, 회원 데이터에 접근하는 계층을 따로 만드는 것이다. 그렇게 해서 회원 저장소라는 인터페이스를 만든다.
(회원 데이터는 자체 DB 를 구축하거나, 외부 시스템과 연동할 수 있다는 요구사항을 통해 아직 회원 저장 구현체가 무엇이 될 지 알 수 없는 상황이다.)
- 구현은 메모리 회원 저장소, DB 회원 저장소, 외부 시스템 연동 회원 저장소를 만든다. 물론 무엇으로 할지는 미확정이다.
- 아직 미확정이기에 일단은 정말 간단하게 구현할 수 있는 메모리 회원 저장소를 만들어서 개발을 진행한다.
* 회원 클래스 다이어그램
- 그림을 간략하게 하기 위해 외부 시스템 연동 회원 저장소에 대한 클래스 다이어그램은 생략하였다.
* 회원 객체 다이어그램
- 실제 서버에 올라올 경우 객체간의 참조들이 어떻게 되는지 그려져 있다.(일단은 메모리 기반의 회원 저장소를 활용한다.)
- 여기서 회원 서비스는 MemberServiceImpl 이다.
이제 본격적으로 회원 도메인을 개발해보자.
코드 작성
다음과 같은 코드들을 hello.core.member 패키지 아래에 작성하자.
- Grade.java : 회원 등급 표현 클래스
public enum Grade {
BASIC,
VIP
}
- Member.java : 회원 클래스
public class Member {
private Long id;
private String name;
private Grade grade;
public Member(Long id, String name, Grade grade){
this.id = id;
this.name = name;
this.grade = grade;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Grade getGrade() {
return grade;
}
public void setGrade(Grade grade) {
this.grade = grade;
}
}
- MemberRepository.java : 회원 저장소 인터페이스
public interface MemberRepository {
void save(Member member);
Member findById(Long memberId);
}
- MemoryMemberRepository.java : 회원 저장소 구현 클래스
public class MemoryMemberRepository implements MemberRepository{
private static Map<Long, Member> store = new HashMap<>();
@Override
public void save(Member member) {
store.put(member.getId(), member);
}
@Override
public Member findById(Long memberId) {
return store.get(memberId);
}
}
- MemberService.java : 회원 서비스 인터페이스
public interface MemberService {
void join(Member member);
Member findMember(Long memberId);
}
- MemberServiceImpl.java : 회원 서비스 구현 인터페이스
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
이와 같이 코드들을 작성해준 다음 코드가 잘 동작하는지 확인해보자.
hello.core 패키지에 아래와 같은 코드를 작성하고 실행해보자.
- MemberApp.java : 실행 파일
public class MemberApp {
public static void main(String[] args) {
MemberService memberService = new MemberServiceImpl();
// 회원 객체 생성 후 가입 요청
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
// 가입이 제대로 됐는지 확인
Member findMember = memberService.findMember(member.getId());
System.out.println("new member = " + member.getName());
System.out.println("find Member = " + findMember.getName());
}
}
결과 :
new member = memberA
find Member = memberA
위와 같이 코드의 동작이 제대로 수행됨을 확인할 수 있다.
지금까지 작성된 코드 중에는 스프링과 관련된 것들이 하나도 들어있지 않다. 순수한 자바 코드로만 개발한 것이다.
하지만 어플리케이션을 직접 동작 시키면서 코드가 잘 동작하는지 확인하는 것은 좋은 테스트 방법이 아니다.
그렇기에 Junit 을 이용해 테스트 코드를 작성해 볼 수 있다.
다음과 같이 test 폴더에 hello.core.member 패키지를 만든 후, 해당 패키지에 테스트 코드를 작성해보자.
- MemberServiceTest.java : 회원 서비스 테스트 코드 작성 파일
public class MemberServiceTest {
MemberService memberService = new MemberServiceImpl();
@Test
void join(){
//given
Member member = new Member(1L, "memberA", Grade.VIP);
//when
memberService.join(member);
Member findMember = memberService.findMember(1L);
//then
Assertions.assertThat(member).isEqualTo(findMember);
}
}
결과 : 테스트 통과
회원 객체를 하나 생성해서 메모리에 저장한 후, 해당 메모리에서 저장한 값을 불러온 다음, 기존에 만들어둔 회원 객체와 비교하여 동일한지 그렇지 않은지를 테스트 하는 코드이다.
코드 실행결과 테스트가 잘 통과됨을 알 수 있다.
설계상의 문제점?
그렇지만 이렇게 만들어진 회원 도메인은 설계상에 문제점이 있다.
* 만일 저장소를 지금과 같은 메모리에 두는 것이 아니라 다른 곳으로 두고자 할 경우, OCP 원칙이 잘 준수될까?
- Repository 구현체가 변경되면 결국 MemberServiceImpl 같은 회원 서비스 구현체에서 생성하여 사용하는 회원 저장소 인스턴스의 코드가 변경되기 때문에 OCP 원칙을 위반하게 된다.
- 즉, 코드의 변경이 일어나는 것이다.
- MemberServiceImpl 클래스에서 new MemoryMemberRepository(); 와 같이 생성한 인스턴스가 new DBRepository(); 와 같은 형태로 코드가 바뀌게 된다.
- 코드가 변경되는건 확장에 열려있고, 변경 및 수정에는 닫혀있어야 한다는 OCP 원칙에 위반된다.
* 또한 지금의 설계는 DIP 원칙을 잘 지키고 있을까?
- DIP 원칙은 구현 클래스에 의존하지 말고, 인터페이스에 의존해야 한다는 원칙이다.
- 그런데 이번 예제에서의 회원 서비스 기능은 인터페이스에 의존하는 동시에, 구현 클래스에 또한 의존하고 있다.
private final MemberRepository memberRepository = new MemoryMemberRepository();
- 위의 코드를 보면 회원 저장소의 구현 클래스가 회원 서비스의 구현 클래스 상에 나타난 것을 확인할 수 있다.
- 이와 같이 어느 한 구현 클래스가 다른 구현 클래스의 존재에 대해 알고 있는 경우, 해당 구현 클래스에 의존하는 형태가 되며, 이는 곧 DIP 원칙을 위반하게 된다.
- 즉, 의존관계가 인터페이스 뿐만 아니라 구현 까지 모두 의존하게 되는 문제점이 있는 것이다.
다음으로는 주문 도메인과 할인정책 도메인을 설계하고 코드를 작성해보자.
'Spring basic' 카테고리의 다른 글
스프링 핵심원리 : 기본편 - 새로운 할인정책 개발과 적용, 그리고 문제점 (0) | 2022.02.10 |
---|---|
스프링 핵심원리 : 기본편 - 주문 및 할인 정책 도메인 설계 (0) | 2022.02.09 |
스프링 핵심원리 : 기본편 - 좋은 객체지향 설계의 5가지 원칙(SOLID) (0) | 2022.02.09 |
스프링 핵심원리 : 기본편 - 스프링과 좋은 객체지향 프로그래밍에 대하여.... (0) | 2022.02.09 |
스프링 입문 : 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - AOP #2 (0) | 2021.11.17 |