Notice
Recent Posts
Recent Comments
Link
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

forDevLife

스프링 입문 - 웹 개발 기초 <2> 본문

Spring

스프링 입문 - 웹 개발 기초 <2>

JH_Lucid 2021. 5. 18. 16:54

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

- 데이터 : 회원 ID, 이름

- 기능 : 회원 등록, 조회

- 아직 데이터 저장소가 선정되지 않음(가상의 시나리오)

 


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

package hello.hellospring.repositiry;

import hello.hellospring.domain.Member;

import java.util.*;

public class MemoryMemberRepository implements MemberRepository {

    private static Map<Long, Member> store = new HashMap<>();
    //sequence 는 key를 생성해주는 것임(0, 1, 2 ...)
    private static long sequence = 0L;

    @Override
    public Member save(Member member) {
        member.setId(++sequence);
        store.put(member.getId(), member);
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        // 결과가 없으면 null, optional로 감싸야 함
//        return store.get(id);
        return Optional.ofNullable(store.get(id));
    }

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

    @Override
    public List<Member> findAll() {
        // 자바에서 실무할 때 List로 많이 쓰므로, 이걸로 반환.
        // hashmap의 member를 values로 쭉 반환해서 ArrayList로 만듬
        return new ArrayList<>(store.values());
    }
}

- 결과 없을 때 Null로 감싸주기 위해 optional 반환형 사용한다.

- optional로 반환된 경우, get()을 통해 Optional<Member>의 Member로 반환해서 받을 수 있다.


3. TestCase 작성 (JUnit)

- 방금 만든 회원 repository class가 원하는대로 동작하는지 검증하는 방법임.

- 코드로 코드를 검증하기!

- 원래는, 자바의 main 메서드를 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당 기능 실행

- 이런 방법은 실행이 너무 오래 걸림. 여러 테스트 한번에 실행하기 어려움

- 자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결

 

* test 주도 개발(TDD) : 테스트를 먼저 만들고, 구현하는 방식

 

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import hello.hellospring.repositiry.MemberRepository;
import hello.hellospring.repositiry.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.*;

// 다른 애들이 가져다 쓸게 아니므로 굳이 public으로 할 필요 없다.
class MemoryMemberRepositoryTest {
    MemoryMemberRepository repository = new MemoryMemberRepository();

    //test 1가지 끝날 때 마다 repository를 비워주는 메서드 추가
    @AfterEach
    public void afterEach() {
        repository.clearStore();
    }


    //test를 추가해서 실행 가능 함. main method 쓰는 거랑 비슷!
    @Test
    public void save() {
        Member member = new Member();
        member.setName("spring");

        repository.save(member);

        //optional(findById의 반환형)에서 get으로 꺼낼 수 있음
        Member result = repository.findById(member.getId()).get();

        //글자로 보는 것보다는, 아래의 Assertions 사용
//        System.out.println("result = " + (result == member));

        //Assertions - by junit
//        Assertions.assertEquals(member, null);

        //Assertions static import 하면, 앞에 Assertion 안써도  - by assertj
        assertThat(member).isEqualTo(result);
    }

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

        //Shift f6을 통해, 겹치는 이름 변경할 수 있음.
        Member member2 = new Member();
        member2.setName("spring1");
        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("spring1");
        repository.save(member2);

        List<Member> result = repository.findAll();

        assertThat(result.size()).isEqualTo(2);
    }
}

- test/java/hello.hellospring에 repository 추가하여 Repository test를 위한 클래스 생성

 

- 다른 곳에서 쓰는게 아니므로, public으로 안해도 된다.

 

- 테스트는 각각 독립적으로 실행되어야 한다. 순서에 의존관계가 있는 것은 나쁜 테스트이다.

 

- @AfterEach : test는 순서대로 돌아가는게 아님. 또한 기존 테스트 내용을 삭제하기 위해 clear 메서드 추가

    -> 해당 어노테이션은 테스트가 끝날 때마다 실행됨!

 

- @Test : 해당 어노테이션이 붙은 메서드가 테스트 시 실행됨. 하나씩도 가능하고, 한꺼번에도 실행 가능

 

- 위에서 설명했던 걸 다시한번 작성하자면, Optional<Member> 반환형으로 지정되어 있을 경우 get()으로 원래 형태로 받을 수 있다.

 

- 직접 결과를 println으로 출력하기보다는, Assertion을 통해서 code의 pass/fail을 확인할 수 있다.

 

- Assertion은 Junit / AssertJ 방식 등이 있으며, AssertJ의 경우, 좀 더 직관적인? 형식으로 작성 가능하다.(아래서 후자가 AssertJ)

   -. Assertions.assertEquals(member, result) == Assertions.assertThat(member).isEqualTo(result)

 

+ Shift F6을 통해, 복사 후 겹치는 이름 변경 가능하다.

+ Option + cmd + V를 통해, 해당 메서드의 반환값을 자동으로 만들어 줄 수 있다.

 

 


4. 회원 서비스 개발

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repositiry.MemberRepository;
import hello.hellospring.repositiry.MemoryMemberRepository;

import java.util.Optional;

public class MemberService {

    private final MemberRepository memberRepository = new MemoryMemberRepository();


    //회원 가입
    public long join(Member member) {
//        가입 할 때, 같은 이름은 안된다는 로직을 추가해보자.
        Optional<Member> result = memberRepository.findByName(member.getName());
        //Optional로 감싸면, 이 안에 멤버가 갇혀 있고, 감싼 덕에 ifPresent 메서드를 사용가능하다.
        //result를 바로 빼서 사용도 가능하다.
        //멤버 값이 있다면, 아래 출력.
        result.ifPresent(m -> {
            throw new IllegalStateException("이미 있는 회원입니다.");
        });

        //아래 처럼 result 없이 바로도 가능하다.
        memberRepository.findByName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 있는 회원입니다.2");
                });


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

- Optional을 이용하면, 이 안에 멤버를 두면서 Optional만의 메서드를 사용할 수 있다.

- get()으로 원래의 멤버(클래스)를 추출 가능하다.

- 여기에서는 ifPresent 메서드를 사용하였다.

 

 

- ctrl + t -> extract method를 통해 지정한 부분을 메서드로 뽑아낼 수 있다.

- 위에서 findByName 부분을 validateDuplicateMember라는 메서드로 변경해서 두었다.

 

* 최종 코드

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repositiry.MemberRepository;
import hello.hellospring.repositiry.MemoryMemberRepository;

import java.util.List;
import java.util.Optional;

//비즈니스 서비스 로직!
public class MemberService {

    private final MemberRepository memberRepository = new MemoryMemberRepository();


    //회원 가입
    public long join(Member member) {
//        가입 할 때, 같은 이름은 안된다는 로직을 추가해보자.
        Optional<Member> result = memberRepository.findByName(member.getName());
        //Optional로 감싸면, 이 안에 멤버가 갇혀 있고, 감싼 덕에 ifPresent 메서드를 사용가능하다.
        //result를 바로 빼서 사용도 가능하다.
        //멤버 값이 있다면, 아래 출력.
        result.ifPresent(m -> {
            throw new IllegalStateException("이미 있는 회원입니다.");
        });

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

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

    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 있는 회원입니다.2");
                });
    }

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

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

- 서비스(비즈니스) 부분은 기존에 작성된 Repository를 활용하는 단계임.

- 따라서 메서드 이름을 비즈니스 스럽게? 직관적으로 작성해야 한다. 

 


5. 회원 서비스 테스트

- 테스트 바로 만드는 법 : cmd + shift + t -> create new test -> test library : Junit

   -> 아래 처럼 자동으로 만들어 줌

- 실제 동작하는 코드는 한글로 적기 애매하지만, 테스트는 한글로 적어도 무방!

- build 시 테스트 코드는 포함되지 않으므로 한글로 작성 가능.

 

* ctrl + option + o -> 안쓰는 import 제거해줌(optimize imports)

  command + p -> parameter info

 


 

- 한글 name으로 테스트 작성

- given / when / then으로 나눠서 작성

- 이 코드에서는 단순하게 hello라는 이름으로 저장 후, 다시 불러와서 비교하는 테스트. 매우 단순하므로 테스트 추가.

- 항상 예외 적인 상황을 테스트 할 수 있어야 함.

 


- 같은 이름으로 들어갈 경우 발생하는 예외 체크하는 테스트

- 회원 서비스에서 중복 예외 발생 시 "이미 있는 회원입니다"라는 예외 메세지를 출력했었음

- try ~ catch를 통해 예외 메세지가 서비스의 예외 메세지와 동일한지 체크

- try ~ catch 말고 다른 방법 제공됨. -> assertThrows 사용

 


- assertThrows 사용

- 오른쪽 람다식이 진행되었을 때, IllegalStateException.class가 발생하게 되면 test pass

 

- assertThrows는 예외를 반환해주므로, assertThat.isEqual을 통해 메세지 확인 가능하다.

 

 

 


MemberService(비즈니스 서비스 로직)에서 사용되는 repository와, Test에서 사용되는 repository 일치를 위해 아래와 같이 작성.

- MemberService에서 repository를 외부에서 생성하여 받을 수 있도록 생성자를 만든다.

- MemberServiceTest에서 테스트 시, 'BeforeEach'에서 repository를 만들어 서비스 로직에 전달하는 방식으로 작성한다.

- 이를 Dependancy Injection(의존성 주입)이라고 한다.

 

 


6. 컴포넌트 스캔과 자동 의존관계 설정

- 멤버 컨트롤러가, 멤버 서비스를 통해 회원가입 / 조회를 해야 한다. -> 의존관계 필요

 

- 녹색 동그라미가 Controller(bean)이다.

- @controller annotation이 있는 Controller를 컨테이너에서 관리하게 됨.

 

 

- 스프링 빈을 등록하는 2가지 방법

1) 컴포넌트 스캔과 자동 의존관계 설정(컴포넌트 + autowire 이용)

2) 자바 코드로 직접 스프링 빈 등록하기

 


1) 컴포넌트 스캔 > 이 어노테이션이 있으면, 스프링이 객체를 생성하여 컨테이너에 등록을 함.

 

@Component : 스프링 빈으로 자동 등록

@Controller : 컨트롤러가 스프링 빈으로 자동 등록된 이유도 컴포넌트 스캔 때문임

 

@Component를 포함하는 다음 애너테이션도 스프링 빈으로 자동 등록된다.

 - @Controller

 - @Service

 - @Repository

Service 정의에 Component 어노테이션이 존재한다.

 

* command + B -> 해당 메서드 / 어노테이션 정의로 이동

 

- 컴포넌트 스캔의 범위는, hellospring 동일 / 하위 패키지까지임.

 

 

- 스프링은 스프링 컨테이너에 스프링 빈을 등록할 때, 기본으로 싱글톤으로 등록한다.(유일하게 하나만 등록해서 공유)

따라서 같은 스프링 빈이면 모두 같은 인스턴스이다. 설정으로 싱글톤 아니게 설정할 수는 있다.

 

+ 생성자에 @Autowired 사용하면 객체 생성 시점에 스프링 컨테이너에서 해당 스프링 빈을 찾아서 주입한다. 생성자가 1개만 있으면 해당 어노테이션을 생략할 수 있다.

package hello.hellospring.controller;

import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

//spring container에서, @Controller 객체를 생성해서 관리하게 됨.
//Spring bean으로 관리된다고 함.
@Controller
public class MemberController {

    // 아래처럼 new를 이용하면, 다른 컨트롤러에서도 memberService를 사용할 수 있게 됨.
    // Spring Container에 등록하기 위해 아래처럼 진행한다.
//    private final MemberService memberService = new MemberService();

    private final MemberService memberService;

    //생성자에 Autowired 써있으면, memberService를 컨테이너에서 가져와서 연결해준다.
    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
}

2) 자바 코드로 직접 스프링 빈 등록하기

: 회원 서비스와 회원 리포지토리의 @Service, @Repository, @Autowire 제거 후 진행

 

* navigate : cmd + o -> 검색하여 메서드로 이동

* 들어가는 parameter 찾기 -> cmd + p

cmd 참고용

 

- HelloSpringApplication(main)위치에 SpringConfig 생성

- 이 안에서 @Bean을 통해 memberService(), memberRepository()를 만들어줘야 함.

- memberRepository는 interface이므로 실제 구현체인 MemoryMemberRepository 생성

 

 

- Controller는 Component Scan & Autowire로 기존과 같이 작성함.(변경사항 없음)

- memberService를 bean에서 연결 -> memberService에서 repository연결

- annotation이랑, 직접 쓰는거랑 장단점이 있다.

 

+ DI 주입 방법

 

1. 생성자 주입 (권장) - 아예 변경 못하게 막을 수 있다.

 

2. 필드 주입(비권장)

3. Setter 주입 : 중간에 바꿀 일 없음에도, public이라 노출됨. 아무 개발자나 setMemberRepository 호출 가능

 

- 의존 관계가 실행 중에 동적으로 변하는 경우는 아예 없으므로, 생성자 주입으로 합시다.

- 실무에서 주로 정형화된 컨트롤러, 서비스, 리포지토리(우리가 작성하는)는 컴포넌트 스캔 사용

 

- 정형화 되지 않고나, 상황에 따라 구현 클래스를 변경해야 하면 설정을 통해 스프링 빈으로 등록

   -> 우리는 repository를 우선 메모리로 만들고, 이를 다른 repository로 바꿔야 한다.

   -> 실제 database에 연결되는 repository로 바꿔야 함

   -> 기존 코드 변경 없이 바꾸기 위해서 스프링 빈으로 등록이 필요하다.

이후에, repository를 config에서 이렇게만 바꿔주면 된다!

 

+ 만약 config에서 스프링 빈으로 등록되지 않았을 경우, Autowired를 통한 DI는 동작하지 않는다.

   아래 처럼 MemberService의 Autowired가 의미가 없어진다는 것이다.

Comments