728x90

/* 이 글은 김영한님의 강의를 보고 정리하려고 작성한 글입니다. */

 

스프링 핵심 원리 - 기본편 | 김영한 - 인프런 (inflearn.com)

 

스프링 핵심 원리 - 기본편 | 김영한 - 인프런

김영한 | 스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., 스프링 핵심 원리를 이해하고, 성장하는 백엔드 개발자가 되어보

www.inflearn.com

 

 

이제 본격적으로 순수 자바코드로 아래 요구사항에 대한 프로젝트를 할 것이다.!

 

 

 

 

회원 도메인

 

 

 

1. 회원 도메인 설계

// Member.class
public class Member {

    //클래스의 멤버변수는 private 처리를 해서 클래스 내에서만 사용 가능하도록, 하지만 getter, setter 를 사용해 외부에서 읽어올 수 있음
    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;
    }
}

 

회원 도메인 협력 관계

◾ 클라이언트의 역할 : 회원서비스 호출

 회원서비스의 역할 : 회원가입 / 회원조희 총 두 가지의 기능을 함

 회원저장소의 역할 : 세가지 저장소 중 하나를 선택하여 회원 관리

   하지만 [ DB 회원 저장소(자체DB) / 외부 시스템 연동 회원 저장소 ] 

      이 둘 중 어떤 저장소를 사용할 지 미확정이기 때문에

  메모리 회원 저장소를 만들어 개발을 진행한다.(로컬 개발, 테스트 진행용)

 

 

 

회원 도메인 협력 관계

 

// MemberService.class 회원서비스 역할 
public interface MemberService {

    void join(Member member);

    Member findMember(Long memberId);
}
// MemberServiceImpl.class 회원 서비스 구현체 
@Component // 각 클래스가 컴포넌트 스캔의 대상이 되도록 에노테이션을 붙여줌
public class MemberServiceImpl implements MemberService{

    private final MemberRepository memberRepository;

    @Autowired // 생성자에서 여러 의존관계도 한번에 주입 가능
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    } //생성자

    /**
     * 회원가입
     */
    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

    /**
     * 회원조회
     */
    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }

    //테스트 용도
    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}
// MemberRepository.class 회원 저장소 역할 
public interface MemberRepository {

    void save(Member member) ;

    Member findById(Long memberId);
}

 회원 서비스 = MemberService <interface>

 회원 서비스의 구현체 = MemberServiceImpl

 회원 저장소 = MemberRepository <interface>

 회원 저장소의 구현체 = MemoryMemberRepository / DbMemberRepository

  MemoryMemberRepository  사용

 

 

💡 여기서 궁금했던 점

: 저장소는 나중에 교체하기위해 Interface로 추상화 하는건 이해가 가나 Service는 교체 가능성이 없는데 추상화를 하는 이유는? 이 궁금했는데 누군가가 인프런에 질문을 했고, 김영한님이 답변한게 있어서 참조

객체 지향 설계와 스프링 마지막에서 김영한님이 말씀했던
* 이상적으로는 모든 설계에 인터페이스를 부여하자
라는 개념으로 인해 서비스에도 추상화를 사용한 것이다.

 

실무 고민
* 하지만 인터페이스를 도입하면 추상화라는 비용이 발생한다.
* 기능을 확장할 가능성이 없다면, 구체 클래스를 직접 사용하고,

  향후 꼭 필요할 때 리팩터링해서 인터페이스를 도입하는 것도 방법이다.


강의에서는 이상적으로 역할과 구현을 분리하는 것에 초점을 맞추어서 이런 부분들도 분리했다.

 

 

 

회원 객체 다이어그램

◾ 객체 다이어 그램 : 실제 서버에 올라왔을 때 객체들의 메모리 참조를 그린 것

◾ 결과적으로 우리가 만들어야 하는 그림

 

 

 

 

 

2. 회원 도메인 개발

 

 

MemoryMemberRepository - 회원 저장소의 구현체

// MemoryMemberRepository.class 회원 저장소의 구현체
@Component // 각 클래스가 컴포넌트 스캔의 대상이 되도록 에노테이션을 붙여줌
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);
    }
}

 

💡 궁금증 1

: store를 만들 때 static을 사용한 이유는?

  static 을 사용하면 객체를 생성하지 않고 클래스 이름으로 직접 접근할 수 있으며, 오로지 단 한 번만 생성되기 때문에         모든 객체에서 동일한 값으로 사용할 수 있다.

  따라서 다른 객체 간에 값을 전달하거나 공유하는데 용이하다.

  이러한 static의 특징을 이용하여 단 하나의 Map을 사용하기 위해 static으로 Map을 생성한 것.

  만약 static을 제거한다면 new MemberRepository(); 를 작성할 때 마다 새로운 Map이 생성되어 객체마다 다른 값의            Map을 가지게 된다.

 

 

💡 궁금증 2

: store를 HashMap으로 생성했는데 일반적으로 동시성 이슈를 방지하기 위해 ConcurrentHashMap으로 생성하는게 좋다고 하셨다. 예제를 단순하게 하기 위해 HashMap으로 생성했는데 둘의 차이점은?

  HashMap ConcurrentHashMap은 둘다 'key-value' 형태의 데이터를 저장하는 자료구조이다.

 

HashMap

  - 데이터의 삽입, 삭제, 검색등의 연산이 빠르다

  - null 값을 저장할 수 있다

  - 싱글스레드 환경에서 안전하게 사용할 수 있다

 

ConcurrentHashMap

  - 내부적으로 데이터를 분할하여 처리하기 때문에 멀티스레드 환경해서 HashMap보다 더 빠른 속도로 동작한다.

  - null 값을 저장할 수 없다

  - 멀티스레트 환경에서 안전하게 사용할 수 있으며 여러 스레드에서 동시에 접근해도 일관성을 유지한다.

 

 

 

 

 

MemberServiceImpl

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

 

💡 궁금증 1

: memberRepository를 만들 때 final로 선언한 이유?

 

  ◾ 불변성(Immutability): final 필드는 한 번 초기화되면 값을 변경할 수 없다. 이것은 필드에 대한 안정성을 제공하며, 필드 값을 보호한다. memberRepository가 한 번 설정되면 다른 객체로 변경되지 않도록 보장한다.

  ◾ 스레드 안정성(Thread Safety): final 필드는 다중 스레드 환경에서 안전하게 사용된다. 여러 스레드가 동시에 memberRepository를 수정하려고 시도하는 것을 방지하며, 동시 접근에 대한 문제를 방지한다.

  ◾ 가독성과 유지보수성: final 키워드를 사용하면 코드의 의도가 명확해진다. memberRepository가 한 번 설정되고 변경되지 않는다는 사실은 코드를 읽는 사람에게 중요한 정보를 전달한다.

  ◾ 오류 방지: final로 선언된 필드는 컴파일 시간에 한 번 초기화되며, 런타임 중에 다시 설정되는 오류를 방지한다.

 

 

 

 

 

3. 테스트 코드 작성

 

위치 중요

◾ 나중에 빌드하면 테스트 코드는 빠진다고 함

 

 

 

 

 

MemberServiceTest

public class MemberServiceTest {
    MemberService memberService;

    // 테스트가 실행하기 전에 무조건 실행
    @BeforeEach
    public void beforeEach() {
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
    }

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

 

◾ Assertions Import할 때 "import org.assertj.core.api.Assertions;" 

 

 

💡 궁금증 1

: Assertions의 기능과 assertThat의 기능 종류

 

  Assertions은 테스트 코드에서 사용되며 특정 조건이 참이어야 테스트가 계속 진행되도록 하는 기능이며

  assertThat은 다양한 비교 및 검증작업을 수행할 때 사용한다.

 

  assertThat 메서드 종류

String ex1 = "hello wolrd";

String ex2 = "hello wolrd";

 assertThat(ex1).isEqualTo(ex2); / isNotEqualTo(ex1); : 두 값이 같은지 / 다른지 비교

 assertThat(ex1).isNull(); / isNotNull(); : 값이 null인지 / null이 아닌지 확인

 assertThat(ex1).istrue(); / isfalse(); : 값이 true인지 / false인지 확인

 assertThat(ex1).startWith("hello") / endsWith("wolrd") : 문자열의 시작, 끝 확인

 assertThat(ex1).contains("hello") : 문자열 특정부분에 포함되는지 확인

등등 많으니까 찾아보기

 

 

 

 

4. 설계의 문제점

 

◾ 이 코드는 의존관계가 인터페이스 뿐만 아니라 구현체까지 모두 의존하는 문제점이 있음

private final MemberRepository memberRepository = new MemoryMemberRepository();

 

  여기서 MemberRepository는 인터페이스에 의존하지만 실제 할당하는 부분은 구현체를 의존하고 있음

  추상화에도 의존하고 구현체에도 의존하고 있는 상태 = DIP 위반

➔ 해결 방안은 '주문'까지 만들고 나서 설명해주신다고 함

728x90

+ Recent posts