* 이 포스트는 인프런에 있는 김영한 님의 '스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술' 강의를 들으면서 내용을 정리한 글입니다. *

 

[메인 컨텐츠]

[무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 인프런 | 강의 (inflearn.com)

 

# 2022-03-04 #

 

#목차#

10-1. 일반적인 웹 어플리케이션 계층 구조

10-2. 비즈니스 요구사항 정리

11. 회원 도메인과 리포지토리 만들기

12. 회원 리포지토리 테스트 케이스 작성


10-1. 일반적인 웹 어플리케이션 계층 구조

 

(1) 컨트롤러(Controller)

웹 MVC의 컨트롤러 역할이다. 다만 나의 경우 MVC 구조가 아니라 REST API 구조로 개발을 진행할 것 같은데, 이 경우에도 컨트롤러라는 명칭과 역할이 유효한지는 아직 모르겠다. 

 

(2) 서비스(Service)

비즈니스 로직을 다루는 부분이다. (계좌 이체, 주문 발송 데이터 추가 등등) 아직 컨트롤러를 제외한 부분은 구현해본 적이 없기 때문에 방대한 웹 어플리케이션에서 서비스와 컨트롤러의 정확한 차이점은 잘 모르겠다. 비즈니스 로직을 구현하는 부분인 만큼 앞으로 제일 복잡해질 부분인 것 같다. 

 

(3) 리포지토리(Repository)

DB에 직접 접근하는 부분이다. DB에 접근하는 코드를 많이 써야할 것으로 예상된다. DB에 접근해서 도메인 객체를 추가/변경/삭제하는 작업이 직접적으로 일어나는 부분이다. 

 

(4) 도메인(Domain)

비즈니스 도메인 객체이다. 비즈니스 로직을 처리하기 위해 만드는 객체를 의미한다. 


10-2. 비즈니스 요구사항 정리 with 새로웠던 부분들

 

(1) DB 설계

강의에서는 아주 간단한 회원가입 형식을 구현할 수 있도록 DB를 설계할 예정이라고 한다. 그러나 내가 목표로 하는 프로젝트는 주문 관리, 배송, 장바구니 등등 다양한 기능이 포함된 비즈니스 로직을 갖췄기 때문에 추후 [스프링 활용 편-1] 강좌를 생각해볼 필요가 있겠다!

+ DB 설계는 누가 하는걸까? 메인 개발자가 하나? 다음에 이 부분을 물어봐야겠다. 

+ 메인 개발자가 확정되었는지, nodeJS 쓰는지 spring 쓰는지도 물어봐야지. 

 

(2) 리포지토리 인터페이스(Repository Interface)

구현 클래스가 아니다. 어떤 DB를 사용할지 확정되지 않은 상황에서 개발을 진행하거나, 여러 개의 DB와 연결해야 하는 상황에서는 인터페이스를 사용한다고 한다. 그래야 구현체를 비교적 쉽게 변경/추가할 수 있기 때문이다. 

+ 프로젝트에서는 어떤 DB를 사용할 예정인지(줌 미팅에서는 1차 개발에서는 DB를 하나만 사용할 거라고 들었다) 물어봐야겠다. 그러나 2차 개발을 한다면 어차피 여러개의 DB를 사용해야 하므로(실제로도 회원 정보 등을 여러 DB에 나눠서 저장하는 것이 꽤 흔하고, 이럴 경우 DB에서 오류 발생 시 어떤 DB/스키마에서 오류가 났는지를 쉽게 알 수 있다는 장점이 있다. <-> 단점은 복잡하다는 것), 리포지토리 인터페이스를 알아두면 언젠가는 분명 도움이 될 것 같다. 


11. 회원 도메인과 리포지토리 만들기

 

순서는 

[ 도메인 클래스 만들기 -> 리포지토리 인터페이스 만들기 -> 리포지토리 구현체(메모리ver) 만들기 ] 이 순서로 진행했다. 

 

1) 도메인 클래스 Member.java

크게 Long(Wrapper 클래스) 타입의 id 필드와 string 타입의 name 필드로만 이뤄져 있다. (로그인이 없는 초간단 구조...)

 

보통 회원가입을 진행하는 경우에도, 서버에서 회원을 구분하는 목적으로 id라는 필드를 선언한다. 즉 '로그인에서 사용자가 입력하는 아이디'와 '서버에서 사용자를 분류하거나 찾기 위한 목적의 아이디'는 서로 다르다. 여기서의 id 필드는 서버에서 구분하기 위한 목적의 필드이다. 

 

또한 실제 서비스에서는 아이디를 long이 아닌 AtomicLong 타입으로 주로 선언한다고 한다. 이유는 '동시성 문제' 때문이란다. (회원가입이 동시에 일어나는 경우 등등에서는 AtomicLong 타입이 문제를 발생시키지 않는다는 것 같다. Map의 경우에도 ConcurrentHahMap을 사용해야 동시성 문제가 발생할 가능성이 없다고 한다. )

 

회원 관련 정보이므로 필드들은 private 타입으로 설정하고, 대신에 getter, setter 메소드가 있다. 

 

2) 리포지토리 인터페이스 MemoryRepository.java

객체로 DB에 접근한다고 가정할 때 필요한 기본적인 메소드를 '선언'했다. (정의는 구현체에서 한다. 인터페이스는 선언만!) 

회원을 저장하는 save(), id로 회원을 찾는 findById(), name으로 회원을 찾는 findByName(), 등록된 모든 멤버를 보여주는 findAll() 메소드를 선언했다. 

 

다만 findById(), findByName() 메소드의 경우 찾는 회원이 없으면 null 값이 반환될 수 있다. 이 경우 null은 메소드의 리턴 타입인 Member과 달라서 오류가 날 수 있다. 

하지만 Java 8 이상에서는(맞나?) Optional<> 문법을 제공한다. Optional<타입>이란, 메소드가 해당 타입을 리턴할 수도 있고 null을 리턴할 수도 있을 때 사용한다. 이 경우에는 Optional<Member>를 사용해서, 주어진 정보에 해당하는 회원이 있으면 Member 타입, 없으면 null도 리턴될 수 있도록 선언했다. 

 

3) 리포지토리 구현체 MemoryMemberRepository.java

인터페이스에서 선언만 해놓은 메소드들을 override해서 재정의한다.

강의에서는 DB가 없는 상황을 가정했기 때문에, DB 대신으로 저장소 역할을 할 store라는 private static 이면서 Map 타입인 객체를 선언한다. Map 타입인 이유는 앞서 도메인에서 사용자를 구분하기 위한 목적으로 선언한 id 필드를 key 값으로 하고, member 객체를 value로 둬서 사용자를 조회하기 편리하게 만들기 위해서이다. 다만 Map은 구현체가 아니라 인터페이스이므로, HashMap 구현체를 사용한다. 

private static Map<Long, Member> store = new HashMap<>();

즉 이런 코드가 된다. 

+ HashMap을 사용할 거면 앞의 Map 부분 대신에 HashMap 타입으로 필드를 만들면 되지 않을까? 왜 Map 타입으로 선언했는지는 잘 모르겠다. (이 부분이 교재에 있었는데 찾아봐야겠다.)

 

실제로 findById()와 findByName() 메소드는 이 store 저장소 객체를 이용해서 사용자 정보를 조회한다. 

 

findById()의 경우 map의 key 값이므로 간단하게 get(key value) 메소드를 이용해서 조회가 가능하다.

그러나 findByName()의 경우는 매개변수로 주어지는 string name은 map의 key 값이 아니므로 바로 조회할 수 없다. 강의에서는 그래서 람다식을 사용했다. .stream(), .filter() 메소드를 통해서 매개변수로 주어진 name과 이름이 같은 멤버 객체를 조회하고 리턴하는 코드이다. 

 

save() 메소드의 경우 보통은 저장한 객체를 다시 리턴해준다. 

 

findAll() 메소드의 경우, 여러 객체를 한번에 리턴해야 하기 때문에 List 타입을 사용했다.

(정확히는 List 인터페이스를 받은 ArrayList 타입. 아까의 Map 경우랑 똑같다. 왜 앞 부분에도 ArrayList 타입이 아니라 List 타입으로 선언했는지를 잘 모르겠다. )


12. 회원 리포지토리 테스트 케이스 작성

 

앞서 스프링이 별도로 테스트를 위한 패키지와 라이브러리를 제공하는 만큼 테스트는 매우매우 중요하다고 언급했다. 따라서 11강에서 작성한 리포지토리 구현 클래스의 메소드들이 잘 작성되었는지를 테스트 해 보려고 한다. 

 

테스트의 장점은 (1) 메인 메소드를 매번 실행할 필요가 없고, (2) 수십개의 테스트를 한 번에 돌릴 수 있으며, (3) 실제 DB에 연결된 클래스 등을 테스트할 때 DB 등의 자료가 변경될 위험이 없는 등 여러 가지가 있다. 

+ 실제 DB를 연결하는 경우는 어떻게 테스트 코드를 짜는지 궁금하다. 임시 DB를 만드려나? 아니면 강의에서처럼 DB역할을 대신할 객체를 만드는 걸까?

 

테스트 코드는 src>test 디렉토리 내부에 작성하며, 여러 파일이 있을 경우 src>main 내부와 비슷한 구조로 작성한다. 강의에서는 리포지토리 클래스 하나만 테스트를 해 보았다. 또한 관용적으로 테스트 클래스의 이름은 테스트하려는 클래스의 이름 + Test 를 붙인다. 예를 들어, 여기서는 MemoryMemberRepositoryTest.java 파일이 작성된 셈이다. 

 

테스트 클래스 내부에서는 우선 테스트하려는 클래스 객체를 만든다. 하나의 테스트 클래스에서 여러 테스트 메소드를 작성할 수 있다. 이때 각 테스트 메소드 위에다 반드시 @Test annotation을 붙여야 한다. 그래야 스프링이 해당 메소드를 테스트로 인식한다. 

 

또한 테스트에서는 System.out.println() 등으로 화면에 직접 출력을 하지 않는다. 대신 Assertions 또는 assertThat 등의 외부 테스트 라이브러리의 메소드를 가져온다. 그래서 테스트를 실행해 보면 별도의 출력이 없다. 잘 실행되면 초록색 체크 표시가 뜨고, AssertThat 등의 조건과 맞지 않는 결과가 출력될 경우에는 에러가 뜬다. 

+ 강의에서는 항상 테스트 메소드의 맨 마지막에 Assertions 또는 AssertThat 코드가 배치되는데, 꼭 이 코드가 마지막에 배치되어야 하는지도 궁금하다. 

 

여러 개의 테스트 사이에는 반드시 순서나 의존관계가 없어야 한다. 스프링에서는 여러 개의 테스트가 있을 때 실행순서가 랜덤이다. 어느 순서로 테스트하던지에 상관없이, 에러가 발생하지 않아야 한다. (ex. 테스트 1의 결과에 따라서 테스트 2의 결과가 달라지는 경우 등등) 또한 DB 등 저장소 역할을 하는 객체에 접근하는 경우, 메모리를 초기화하지 않으면 이전 테스트에서 남긴 데이터/객체가 다음 테스트의 결과에 영향을 줄 수 있다. 따라서 하나의 테스트가 끝나고 나면 메모리를 비워주는 작업이 필요하다. 

 

스프링에서는 @AfterEach annotation을 붙인 메소드를 사용하면 각 테스트가 끝날 때마다 해당 메소드를 실행시킨다. 보통 이런 메소드에는 데이터 저장소의 초기화 코드가 들어간다. 

 

또한 예제 코드를 보니 테스트를 꼭 메소드 수 만큼 만들 필요는 없는 것 같다. 그냥 테스트하려는 부분에 초점을 맞춰서 필요한 만큼의 테스트를 만들면 될 것 같다. 

그리고 테스트 메소드는 외부에서 따로 매개변수를 받지 못하므로, 기존 메소드의 리턴 타입을 쓰기보다는 매개변수도 없고 리턴 타입도 void를 사용하는 것 같다. 

 

'server-side > spring' 카테고리의 다른 글

spring 개발일지 18-19강  (0) 2022.03.08
spring 개발일지 15-16강  (0) 2022.03.05
spring 개발일지 5-6강  (0) 2022.03.03
spring 개발일지 4강  (0) 2022.03.03
spring 개발일지 1-3강  (0) 2022.03.02

+ Recent posts