BackEnd/Spring 🍃

[Spring] 5. 좋은 객체지향 프로그래밍이란 Ⅱ : 실습편 - 회원 도메인

SLYK1D 2025. 3. 12. 22:13
728x90
반응형

1. 요구사항 정의하기

이번 장에서는 이전에 예고드린 데로 앞 장에서 다룬 이론적인 내용을 적용해 보기 위해서 회원별로 주문 및 할인정책을 적용하는 예제를 만들어 보겠습니다. 참고로 이번 예제에서는 Java 만을 사용해서 예제를 구현해 보고, 이후에 스프링을 어떻게 바꾸는 지를 같이 알아볼 예정입니다.

시작에 앞서 먼저 전체적인 밑그림을 그리기 위해 요구사항을 정의하는 것부터 시작하겠습니다. 이번 예제에서는 회원에 대한 도메인과 주문/할인 정책에 대한 도메인으로 각각 나눠서 살펴볼 것입니다. 먼저 회원에 대해서는 아래와 같은 요구사항을 요청했다고 가정해 봅시다.

[비즈니스 요구사항 - 회원]
1. 회원을 가입하고, 조회할 수 있다.
2. 회원은 일반 등급과 VIP 등급이 있다.
3. 회원 데이터는 자체 DB를 구축할지, 외부 시스템과 연동할지 미확정인 상태이다.

다음으로 주문 및 할인 정책에 대해 요구사항입니다.

[비즈니스 요구사항 - 주문 및 할인 정책]
1. 회원은 상품을 주문할 수 있다.
2. 회원 등급에 따라 할인 정책을 적용할 수 있다.
3. 할인 정책은 모든 VIP 등급에 대해 1000원을 할인해 주는 고정 금액 할인으로 적용한다. (추후에 변동 가능)
4. 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. 최악의 경우, 할인 정책을 적용하지 않을 수도 있다. (미확정)

위의 요구사항 내용을 모두 보면, 회원 데이터나 주문 및 할인 정책에 지금 당장 결정하기 어려운 부분들이 존재합니다. 하지만, 이런 정책이 결정될 때까지 개발을 무기한 기다릴 수도 없는 상황이기 때문에, 최대한 유연하게 대응할 수 있도록 설계하는 것이 중요합니다. 그러면 이번 예시를 어떻게 설계할 지에 대해 회원 도메인부터 하나씩 설계해 보도록 하겠습니다. 추가적으로 이번 예제는 순수하게 자바로만 먼저 개발해 보고, 이후에 스프링을 적용하는 방식으로 예제에 대한 개발을 진행하겠습니다.

 

2. 회원 도메인 설계

2.1 요구사항 확인하기

먼저 회원 도메인을 설계하기에 앞서, 요구사항을 다시 한번 살펴보도록 하겠습니다.

[비즈니스 요구사항 - 회원]
1. 회원을 가입하고, 조회할 수 있다.
2. 회원은 일반 등급과 VIP 등급이 있다.
3. 회원 데이터는 자체 DB를 구축할지, 외부 시스템과 연동할지 미확정인 상태이다.

위의 요구사항에서 가장 문제가 되는 부분이 있다면, 3번의 내용처럼 데이터를 저장할 방법이 확정되지 않은 상태에서 개발해야 된다는 것입니다. 때문에, 이를 해결하고자 회원도메인을 설계할 때, ① 메모리에 저장하는 방법, ② 데이터베이스에 저장하는 방법에 대해서 고려해 보도록 하겠습니다. 

앞서 말한 내용을 토대로 클래스별 관계도를 그리면, 아래와 같이 표현하게 됩니다. 

클래스 간 관계도

위의 관계도에서처럼 회원정보를 저장하는 클래스인 MemberRepository에 대해 인터페이스로 구현하고, 이를 메모리에 저장하는 MemoryMemberRepository 클래스나 데이터베이스에 저장하는 DbMemberRepository 클래스로 구현해 두면 저장방식이 변경되어도 대응이 가능한 상태로 개발 및 운영을 할 수 있게 됩니다. 

다음으로 위의 요구사항에서 회원 가입과 조회가 가능하다는 내용이 있는데, 이를 MemberService라는 인터페이스에 정의해 두고, 구현하기 위해서 MemberServiceImpl이라는 클래스로 구현할 것입니다. 결과적으로 위의 클래스로부터 생성되는 객체들의 연관관계를 그려보면 아래 그림처럼 표시할 수 있습니다. 

객체 간 관계도

그 외 나머지 요구사항에 대해서는 구현을 하면서 조금씩 이야기해보도록 하겠습니다.

 

2.2 실습: 회원 클래스 구현하기

이제 본격적으로 회원 도메인을 구현해 보도록 하죠. 먼저 Member 클래스를 생성해서 클라이언트로부터 전달받을 정보를 받아줄 클래스를 생성해 보겠습니다. 예시이기 때문에, 회원 정보에 대해서는 회원 ID, 회원 이름, 등급으로 구성하겠습니다. 구체적인 코드는 다음과 같습니다. 

[member/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;
    }
}

다음으로 회원 정보에도 등장했던 등급에 대해서 "Grade"라는 이름의 enum 클래스를 만들어 주겠습니다. 앞서 요구사항에 보면 일반과 VIP등급으로 구분한다고 했기 때문에, 일반은 "Basic"으로, VIP 등급은 "VIP"로 구현하겠습니다.

[member/Grade.java]

public enum Grade {
    BASIC,
    VIP
}

 

2.3 실습: 회원 레포지토리 구현하기

다음으로 회원 정보를 저장하기 위한 회원 레포지토리를 구현하기 위해 "MemberRepository"라는 인터페이스를 생성하도록 하겠습니다. 구체적인 코드는 다음과 같습니다.

[member/MemberRepository.java]

public interface MemberRepository {

    // 회원정보 저장 메소드
    void save(Member member);

   // 회원정보 조회 메소드
    Member findById(Long id);
}

MemberRepository 인터페이스에는 회원 도메인의 기능인 회원 가입과 회원 조회 기능이 있었기 때문에, 가입하면서 넘어오는 정보를 저장하기 위해 save() 메서드를, 회원 조회를 하기 위해 회원ID를 이용한 조회를 하기 위해 findById() 메소드를 생성하였습니다.

인터페이스까지 생성했으니, 먼저 메모리 기반의 저장소를 구현해 보겠습니다. 의미에서도 눈치채신 분들이 있겠지만, 해당 저장소는 실행하는 컴퓨터 혹은 서버의 전원이 꺼지면, 저장된 정보가 날아가는 단점이 있습니다. 하지만, 개발용이고, 개발을 하는 현시점에는 미정이지만, 추후에 데이터베이스나 외부저장소를 활용할 수 있기 때문에 임시 목적으로 생성하는 성격이 강합니다. 

아무튼 MemoryMemberRepository 구현 클래스를 생성해 보도록 하겠습니다. 구체적인 코드는 다음과 같습니다.

[member/MemoryMemberRepository.java]

import java.util.HashMap;
import java.util.Map;

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);
    }
}

위의 예시에서 회원정보 저장을 하고자, HashMap을 사용했는데, 실습예제이기 때문에 사용하였으며, 실무에서 사용하려면 동시성 문제가 발생할 수 있기에 지양합니다. 어디까지나 개발용 및 테스트용으로만 활용하시면 좋습니다. 

여기까지 하면, 회원 저장소에 대한 구현은 완료했습니다. 앞서 데이터베이스와의 연결을 위한 클래스도 구현한다고 말씀드렸는데, 이후에 다룰 IoC, DI에 대한 개념을 설명하기 위해서 이번 장에서는 구현하지 않습니다.

 

2.4 실습: 회원 서비스 구현하기

마지막으로 회원서비스를 구현해 보겠습니다. 회원 서비스는 MemberService라는 인터페이스를 구현하고, 이를 구현할 클래스인 MemberServiceImpl 클래스를 생성하겠습니다. 또한 해당 클래스 및 인터페이스는 비즈니스 요구사항을 구현해 주는 클래스이기 때문에 구성 메서드로 회원 가입을 위한 join() 메서드와 회원 조회를 위한 findMember() 메서드를 선언하도록 합니다.

[member/MemberService.java]

public interface MemberService {

    // 회원 가입 메소드
    void join(Member member);

    // 회원 조회 메소드
    Member findMember(Long memberId);
}
[member/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);
    }
}

이렇게 하면, 회원 도메인에 대해 개발이 완료되었습니다. 하지만, 우리가 구현한 코드들이 정상적으로 동작하는지 확인을 해봐야 하며, 아래에서 위의 코드들에 대한 테스트를 진행해 보도록 하겠습니다.

3. 예제 코드 테스트 하기

앞서 설명드렸듯이, 작성한 코드가 정상적으로 실행되는지를 확인하기 위한 테스트 코드를 작성하고 실행해 보겠습니다. 회원 도메인이기 때문에 회원가입이 잘 저장되고, 조회 메서드를 호출했을 때 정상적으로 출력되는 가를 확인하면 됩니다. 먼저 코드를 작성하고서, 이 후 설명을 이어가겠습니다. 

[member/MemberServiceTest.java]

import hello.core.AppConfig;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class MemberServiceTest {

    MemberService memberService = new MemberServiceImpl(new MemoryMemberRepository());

    @Test
    void joinAndFindMemberTest() {
        // given
        Member member = new Member(1L, "memberA", Grade.VIP);

        // when
        memberService.join(member);
        Member findMember = memberService.findMember(1L);

        // then
        Assertions.assertEquals(member, findMember);
    }
}

위의 코드에서 중요하게 볼 부분은 크게 2가지가 있는데, 먼저 join() 메소드 내에 있는 주석 부분을 보겠습니다. 위의 구조는 "Given - When - Then 구조"라고 해서 쉽게 말하면, 가정을 한다고 생각하시면 이해하기 쉬울 겁니다. 먼저 가정을 하려면 "어떤 대상이 어떤 상황 혹은 행동을 한다"에서 시작합니다. 위의 구조에서도 "어떤 대상"에 해당하는 객체를 Given위치에 선언해 줍니다. 여기서는 회원 A가 되며, 해당 회원의 등급을 VIP라고 가정해 봅시다. 

다음으로 "어떤 행동 혹은 상황"이 주어져야 합니다. 회원 도메인에서는 회원의 가입 및 조회를 담당하기 때문에 회원이 가입하거나, 회원 조회를 하는 상황이 이에 해당한다고도 볼 수 있습니다. 이를 위해 When의 위치에 먼저 회원 서비스 내의 join() 메서드를 호출해서 회원 가입을 진행하고, 현재 조회가능한 회원을 회원ID 로 조회하는 findMember() 메소드를 실행합니다. 그리고 메소드를 실행한 결과로 조회된 회원의 정보를 findMember 객체에 대입합니다. 

끝으로 findMember 객체의 회원이 앞서 가입한 회원정보와  동일한 지를 확인하기 위한 검증 과정이 필요합니다. 이를  Then 부분에 작성하면 되는데, 두 번째로 중요하게 볼 부분인 Assertions 문을 사용하면 됩니다. 해당 구문은 JUnit을 통해 테스트를 진행할 때, 위와 같이 결과에 대한 동일한지의 진위여부를 파악할 때 활용하면 좋으며, 실행하면 아래 그림처럼 별다른 에러 메시지 없이 종료될 경우를 성공으로 보면 됩니다.

여기까지 회원 도메인에 대한 실습이었으며, 다음 장에서는 주문 및 할인 정책에 대한 요구사항과 그에 대한 실습 코드를 구현해 보도록 하겠습니다.

728x90
반응형