IT recording...

[Spring] 회원 관리 예제 (Controller, Service, Repository, Domain의 역할) + Singleton,DI,IoC/ Optional,Assertions / JUnit test 본문

Spring

[Spring] 회원 관리 예제 (Controller, Service, Repository, Domain의 역할) + Singleton,DI,IoC/ Optional,Assertions / JUnit test

I-one 2021. 3. 8. 14:48

 

1. 비즈니스 요구사항 정리

* 데이터 : 회원ID(자동생성), 이름

* 기능 : 회원 등록, 조회

* 아직 데이터 저장소가 선정되지 않음

 

2. 기본 웹 어플리케이션 계층 구조 

- Controller : 웹 MVC의 컨트롤러 역할

- Service : 핵심 비즈니스 로직 구현

- Repository : 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리

- Domain : 비즈니스 도메인 객체, ex) 회원,주문,쿠폰 등등 주로 데이터베이스에 저장하고 관리됨

 

Controller -> Service -> Repository -> DB

                      |

                      V

                   Domain

(4개의 정확한 역할에 대한 느낌이 와닿지 않는다. 아래의 예제를 통해 느낌을 확실히 해보자)

 

3. 코드 작성하기

 

1) Controller

- 웹 사이트에서 받은 사이트 주소를 Mapping 시키고, 받은 정보들 Service에 전달해주는 배달원 역할을 수행한다.

(주소 찾아서 정보 전달)

controller/HomeController

//HomeController
@Controller
public class HomeController {
    @GetMapping("/")
    public String home() {
        return "home";
    }
}

// templates/home.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
    <div>
        <h1>Hello Spring</h1>
        <p>회원 기능</p>
        <p>
            <a href="/members/new">회원 가입</a>
            <a href="/members">회원 목록</a>
        </p>
    </div>
</div> <!-- /container -->
</body>
</html>

controller/MemberController

@Controller
public class MemberController {
    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService){
        this.memberService = memberService;
    }

//GetMapping("/members/new")로 들어오면 templates/members/createMemberForm 주소 찾아서 이동 시켜 줌 
//> createMemberForm에서 Post로 넘긴 정보를 @PostMapping("/members/new")로 받아서 처리

    @GetMapping("/members/new")
    public String createForm(){
        return "members/createMemberForm";
    }

    @PostMapping("/members/new")
    public String create(MemberForm form){

        Member member = new Member();
        member.setName(form.getName());
        memberService.join(member);

        return "redirect:/"; //home으로
    }

//GetMapping("/members")로 들어오면 templates/members/memberList로 이동
//model.addAttribute의 key,value형태로 정보 전달
    @GetMapping("/members")
    public String list(Model model){
        List<Member> members = memberService.findMembers();
        model.addAttribute("members", members);
        return "members/memberList";
    }
}

 

// templates/members/createMemberForm.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
    <form action="/members/new" method="post">
        <div class="form-group">
            <label for="name">이름</label>
            <input type="text" id="name" name="name" placeholder="이름을 입력하세요">
        </div>
        <button type="submit">등록</button>
    </form>
</div> <!-- /container -->
</body>
</html>

// templates/members/memberList.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
    <div>
        <table>
            <thead>
            <tr>
                <th>#</th>
                <th>이름</th>
            </tr>
            </thead>
            <tbody>
            <tr th:each="member : ${members}"> <!--th:each = 루프 돌면서 로직 실행-->
                <td th:text="${member.id}"></td>
                <td th:text="${member.name}"></td>
            </tr>
            </tbody>
        </table>
    </div>
</div> <!-- /container -->
</body>
</html>

 

 

* model을 통한 데이터 전달 관련 블로그

 

2) Domain

- 쉽게 말하면 객체. 회원(id,name)/ 주문(id,order,member) /쿠폰(id,store,member) 등.

- DB에 저장되어 관리되는 애들을 말한다. 

 

domain/Member

package hello.hellospring.domain;

public class Member {

    private Long id;
    private String name;

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

 

3) Repository

- 직접 DB를 건드는 애들. save, delete, find 등의 일이 수행된다. 

- 아직 db가 확정되지 않은 상황을 가정했으므로, 코드 바꿔치기가 수월하게 interface를 사용해서 작성하였다. 

 

repository/MemberRepository

public interface MemberRepository {
 Member save(Member member);
 Optional<Member> findById(Long id);
 Optional<Member> findByName(String name);
 List<Member> findAll();
}

repository/MemoryMemberRepository

@Repository
public class MemoryMemberRepository implements MemberRepository{
    private static Map<Long, Member> store = new HashMap<>();
    private static long sequence = 0L;

//sequence이용해서 id 관리(auto increment)
    @Override
    public Member save(Member member) {
        member.setId(++sequence);
        store.put(member.getId(),member);
        return member;
    }

//Optional이라는 반환형을 사용한다. (Null값 처리에 용이)
    @Override
    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }

//자바의 람다식 
//map.values() : map에 존재하는 모든 value들 집합 return
//.stream() : iterator한 작업을 수행 가능하게 해준다. + 병렬처리 가능
//.filter() : 람다식을 이용해서 필요한 정보만 뽑아내기
//.findAny() : 찾은 것 중 맨 처음꺼 꺼내기

    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream()
                .filter(member -> member.getName().equals(name))
                .findAny();
    }

    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }

//Test시 저장소의 정보들을 정리해주기 위한 메소드
    public void clearStore(){
        store.clear();
    }
}

 

- Optional 자료형

: 예상치 못한 Null 관련 예외를 회피할 수 있다. 

> 생성 : of(), ofNullable(value), empty() 메소드를 사용하여 Optional 객체를 생성한다. 

> 접근 : get(), orElse(T other), orElseGet()

// ofNullable, orElse 사용
Optional<String> maybeCity = Optional.ofNullable(cities.get(4)); // Optional
int length = maybeCity.map(String::length).orElse(0); // null-safe
System.out.println("length: " + length);

//null값이 반환될 수 있는 상황에서의 사용
public static <T> Optional<T> getAsOptional(List<T> list, int index) {
	try {
		return Optional.of(list.get(index));
	} catch (ArrayIndexOutOfBoundsException e) {
		return Optional.empty();
	}
}

//ifPresent 사용 (람다식과 콜라보레이션)
Optional<String> maybeCity = getAsOptional(cities, 3); // Optional
maybeCity.ifPresent(city -> {
	System.out.println("length: " + city.length());
});

 

Optional 관련 블로그

 

 

- stream 관련 부가 정보

더보기

 

 

 

4) Serive

- 회원 가입, 조회 등의 비즈니스 단 기능들이 수행되는 곳. 중복 검사 등도 이 곳에서 실행된다. 직접 DB에 접근하는 일은 없다. (repository를 거치기 때문)

 

service/MemberService

@Service
public class MemberService {

    private final MemberRepository memberRepository;

    @Autowired
    public MemberService(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    }

    /**
     * 회원가입
     */
    public Long join(Member member) {

        validateDuplicateMember(member); //중복 회원 검증

        memberRepository.save(member);
        return member.getId();
    }

    //ctrl+ alt + shift + T : 여러가지 나옴
    //ctrl + alt + m : 메소드 뽑아내기
    //같은 이름이 있는 중복 회원X
    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });
    }

    /**
     * 전체 회원 조회
     */
    public List<Member> findMembers(){
        return memberRepository.findAll();
    }

    public Optional<Member> findOne(Long memberId){
        return memberRepository.findById(memberId);
    }
}

 

 

4. Test하기 

- 자바에는 클래스 별로 test를 할 수 있는 환경이 존재한다.

 

실제 코드 빌드에는 들어가지 않으나 작은 단위로 쪼개어 프로그램이 정상 작동하는지를 확인할 수 있다.

 

- @Test 태그를 이용하여 식별하는데, 작성된 Test코드 중 어느 것이 먼저 작동할 지는 랜덤이다. 

따라서 같은 repository를 사용하는 경우 한 method가 실행 된 후 저장소를 깔끔히 청소해 놓아야 다음 method가 실행시켰을 때 정상 작동 테스트가 정상적으로 가능하다. 

--> @AfterEach 태그를 통해 각 method들이 끝난 후 메모리를 정리할 수 있는 코드를 작성했다. 

 

- Assertions

: 조건문을 단순화하고 반복적인 코드를 줄이는 역할을 한다. 

assertEqulas(expected,result), assertNull(x), assertNotNull(x) 등

 

test/java/hello.hellospring.repository/MemoryMemberRepositoryTest

public class MemoryMemberRepositoryTest {
    MemoryMemberRepository repository = new MemoryMemberRepository();

    //test가 끝난 후 메모리 정리해주는 메소드 , 테스트들 중 어떤 것이 먼저 실행될 지는 보장 되지 않는다.
    @AfterEach
    public void afterEach(){
        repository.clearStore();
    }

    @Test
    public void save(){
        Member member = new Member();
        member.setName("spring");

        repository.save(member);
        Member result = repository.findById(member.getId()).get();
        Assertions.assertEquals(member,result);
    }

    @Test
    public void findByName(){
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);

        //shift+F6 : rename편하게
        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);

        Member result = repository.findByName("spring1").get();
        assertThat(result).isEqualTo(member1);
    }

    @Test
    public void findAll(){
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);

        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);

        Member member3 = new Member();
        member3.setName("spring3");
        repository.save(member3);

        List<Member> result = repository.findAll();
        assertThat(result.size()).isEqualTo(3);
    }
}

 

test/java/hello.hellospring.service/MemberServiceTest

class MemberServiceTest {

//아래와 같이 하면 객체간의 종속성이 존재하여, test와 실제 service에서 사용하는 repository가 같지 않게 될 수도 있다.
	/*MemberService memberService = new MemoryMemberRepository();
    MemoryMemberRepository memberRepository = new MemberService(memberRepository);*/

    MemberService memberService;
    MemoryMemberRepository memberRepository;

//BeforeEach 태그를 활용해 DI(dependency injection)의존성을 강제로 주입해준다. 
//Service의 생성자를 통하여 repository를 강제로 주입
    @BeforeEach
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository);
    }

    @AfterEach
    public void afterEach(){
        memberRepository.clearStore();
    }

    @Test
    void join() {
        //given
        Member member = new Member();
        member.setName("spring");

        //when
        Long saveId = memberService.join(member);

        //then
        Member findMember = memberService.findOne(saveId).get();
        Assertions.assertThat(member.getName()).isEqualTo(findMember.getName());
    }

    @Test
    public void duplicateException(){
        //given
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring");

        //when
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

        /*try{
            memberService.join(member2);
            fail();
        }
        catch(IllegalStateException e){
            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다12.");
        }*/

        //then
    }

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

 

 

- Sigleton

: 어플리케이션 실행 시 특정 클래스가 메모리를 최초 한번만 할당하여, 그 메모리에 인스턴스를 만들어 사용하는 디자인 패턴.

+ 고정된 하나의 메모리를 사용하여 메모리 낭비 방지

+ 메모리 누수 방지

+ 공통된 오브젝트를 사용하는 상황에서 유용

 

img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJpdws%2Fbtqyu9cBA1x%2FB9nmkFBH5K7rBYG8x0V7n1%2Fimg.jpg

 

1) Java singleton - classloader에 의해 싱글톤 패턴을 구현한다. 

: 생성자 private선언(외부에서 클래스의 오브젝트를 생성할 수 없게 함)

: 참조는 static으로 정의(어느 영역에서든 접근이 가능하도록)

+ Thread safety를 위해 >  getter 메소드의 Synchronized 선언

 

2) Spring singleton - spring container에 의해 구현한다.

해당 클래스에 대해 딱 한 개의 인스턴스를 만들며, bean이 호출될 때마다 스프링은 생성된 공유 인스턴스를 return시킨다.

: @Bean,@Controller,@Service,@Repository,@Component

+ 생성자에 @Autowired를 통해 컨테이너에서 스프링이 연관된 객체를 찾아 넣어준다.(DI)

- 과거에는 xml을 통해 작성했지만 최근에는 Annotation으로 흐름이 변경됨.

- SpringConfig 파일 작성을 통해 구현도 가능.

@Configuration
public class SpringConfig {
    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository(){
        return new MemoryMemberRepository();
    }
}

(Controller는 와안전 기본으로 찾아서 하니까 여기에 작성할 필요 없음)

 

 

*참조 블로그

 

 

 

- DI (dependency injection, 의존성 주입)

: Spring의 IoC(제어의 역전) 으로, FrameWork에 의해 객체의 의존성이 주입되는 설계 패턴이다. 

종속성이 감소하여 component들의 속성이 변경되어도 민감하지 않고, test가 용이해진다. 

 

public class MemberService {

//아래와 같이 작성된다면 객체끼리의 종속성이 존재한다.
 /*private final MemberRepository memberRepository = new MemoryMemberRepository();*/

//따라서 외부에서 종속성을 넣어주게끔 코드 변경.
    private final MemberRepository memberRepository;

    //@Autowired
    public MemberService(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    }
    
 }

 

*관련 블로그

 

Comments