스프링부트 ) Inflearn 학습내용 정리 ( 김영한 )
Programming/BACKEND

스프링부트 ) Inflearn 학습내용 정리 ( 김영한 )

반응형

 

목차

     


     

    https://inf.run/6bix

     

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

    스프링 입문자가 예제를 만들어가면서 스프링 웹 애플리케이션 개발 전반을 빠르게 학습할 수 있습니다., - 강의 소개 | 인프런...

    www.inflearn.com

     

     

    1. 필요 프로그램 설치

    1.1. IDE - InteliJ설치

    우선 InteliJ 커뮤니티 버전 (무료버전)을 설치
    (아래 링크 참고)

    2022.05.21 - [Programming/기초 지식] - Kotlin ) intelliJ IDE 설치, Kotlin 기초

     

    Kotlin ) intelliJ IDE 설치, Kotlin 기초

    목차 1. IntelliJ다운로드 및 설치 2. 프로젝트 생성 다운로드가 완료되면 3. 코틀린 기초 3.1. 출력문 3.2. 리터럴 3.2.1. 리터럴의 종류 리터럴 : 코드 작성시에 값을 작성하는 문법 정수(Int, Long) ..

    montoo.tistory.com


     

    1.2. H2 Database 설치

    다음 링크로 들어가 1.4.200버전을 설치 ( 최신 버전은 일부 기능이 정상작동하지 않음 )

    https://www.h2database.com/html/download-archive.html

    설치 후 실행했다면 *~/test.mv.db* 파일을 삭제할 것.
    ( 오류 발생함 )


     

    2. 스프링 프레임워크 빌드하기

    2.1. 스프링에서 제공하는 Spring Initializr을 활용하여 SpringBoot를 빌드한다.

    https://start.spring.io/

     

    위와 같이 세팅하고, 하단의 버튼 'Generate' 를 선택하면 압축된 프로젝트?가 다운로드되는데, 편한 위치에 압축 풀기한다.

    먼저 설치한 인텔리제이를 실행하고, 압축 푼 파일 중 build.gradle 파일을 선택하여 Open

     

    그러면 자동으로 빌드가 진행되고, 필요한 라이브러리들을 자동으로 다운로드한다.

     


     

    2.2. 추가적인 인텔리제이 설정 ( Gradle이 아닌 InteliJ로 run 되도록 )

    위와 같이 설정해야 조금 더 빠르게 실행될 뿐 아니라, 오류가 발생하지 않았다.

     


     

    3. 스프링부트의 라이브러리

    Gradle은 의존관계가 있는 라이브러리를 함께 다운로드 한다.

    3.1. 스프링 부트 라이브러리

    • spring-web-starter-web
      • spring-boot-starter-tomcat : 톰캣(웹서버)
      • spring-webmvc : 스프링 웹 MVC
    • spring-boot-starter-thymeleaf : 타임리프 템플릿 엔진(view)
    • spring-boot-starter ( 공통 ) : 스프링 부트 + 스프링 core + 로깅
      • spring-boot
        • spring-core
      • spring-boot-starter-logging
        • logback, slf4j

     


     

    3.2. 테스트 라이브러리

    • spring-boot-starter-test
      • junit : 제이유닛 ( 테스트 프레임워크 ) 
      • mockito : 모키토 ( 목 라이브러리 )
      • assertj : 어썰트제이 ( 테스트 코드를 좀 더 편하게 작성하도록 도와주는 라이브러리 ) 
      • spring-test : 스프링 통합 테스트 지원

     


     

    4. View 환경설정

    4.1. Welcome Page 만들기 ( index.html )

     src/main/resources/static/index.html

    <!doctype html>
    <html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport"
              content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>index.html</title>
    </head>
    <body>
    Hello
    <a href="/hello">hello</a>
    </body>
    </html>

     


     

    4.2. 컨트롤러 구성

    4.2.1. 컨트롤러 클래스

    src/main/java/hello.hellospring.controller/HelloController.java

    package hello.hellospring.controller;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.GetMapping;
    
    @Controller
    public class HelloController {
    
        @GetMapping("hello")
        public String Hello(Model model){
            model.addAttribute("data", "hello!!");
            return "hello";
        }
        
    }

     

    4.2.2. hello.html 페이지

    src/main/resources/template/hello.html

    타임리프( thymeleaf ) 템플릿 엔진을 사용하는 것이 특징적이다.

    <!doctype html>
    <html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>hello.html</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
        <meta charset="UTF-8">
        <meta name="viewport"
              content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
    </head>
    <body>
    
    <p th:text="'안녕하세요. ' + ${data}" >안녕하세요. 손님</p>
    
    </body>
    </html>

     


     

    5. 빌드하고 실행하기 ( 배포 패키지 생성하기 )

    5.1. cmd를 실행하여 프로젝트 폴더로 이동

     


     

    5.2. gradlew build 명령어 실행

    프로젝트 디렉토리로 이동하였으면 gradlew 파일이 존재하는지 확인하고,
    (windows 기준) gradlew build 명령어를 실행한다.

     

    그러면 build 디렉토리가 생성되는데..
    ./build/libs 로 이동하면 ' hello-spring-0.0.1-SNAPSHOT.jar '파일이 생성되는 것을 알 수 있다.

     


     

    5.3. 배포 패키지(jar파일) 실행해보기

    배포 패키지가 위치한 디렉토리에서
    ' java -jar hello-spring-0.0.1-SNAPSHOT.jar ' 명령으로 실행한다.

     

    그러면 이렇게 스프링이 실행되는 것을 확인할 수 있다.

     


     

    6. 스프링 웹 개발 기초

     

    6.1. 정적 컨텐츠 ( Static Content )

    서버에서 처리하는 것 없이 파일을 웹브라우저에 내려주는 방식

    1. 톰캣 서버는 컨테이너에서 우선적으로 'hello-static'이라는 컨트롤러를 탐색.
    2. 1에서 탐색 결과가 없으면, resources/static 에서 hello-static.html을 탐색 후 존재하면 그것을 브라우저로 반환

     

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>resources/static/hello-static.html</title>
    </head>
    <body>
    정적컨텐츠입니다.<br>
    <h3>' src/main/resources/static/hello-static.html '</h3>
    </body>
    </html>

     


     

    6.2. MVC / 템플릿 엔진

    최근 가장 많이 사용되는 동적 컨텐츠 방식

    View와 Controller의 관심사를 분리하기 위해 발전되었다. 

    • 템플릿 엔진
      • 과거에는 jsp, php등이 많이 쓰임
      • 정적 컨텐츠 처럼 html을 그냥 브라우저로 보내는 것이 아닌, 서버에서 프로그래밍을 하여 html을 동적으로 브라우저로 건내는 방식
      • MVC패턴에서 View(화면, 페이지)를 보여주기 위한 구성 중 하나
    • MVC 패턴
      • Model - View - Controller
      • 위와 같은 방식을 구동하기 위해 MVC패턴을 사용한다.

    특징적인 것은 브라우저에 html을 템플릿 엔진이  렌더링을 하여 변환을 하고, 건내준다는 것

     

    controller

    package hello.hellospring.controller;
    
    ...
    
    @Controller
    public class HelloController {
    
    ...
    
        @GetMapping("hello-mvc")
        public String helloMvc(@RequestParam(value="name"/*, required=false*/) String name, Model model){
            model.addAttribute("name", name);
            return "hello-template";
        }
        
    }

     

    view

    <!DOCTYPE html>
    <html xmlns:th="http://ww.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>hello-template.html</title>
    </head>
    <body>
    
    <!-- thymeleaf의 특징은 태그속성에 쓰여진 값이 태그 사이의 값에 치환된다. -->
        <p th:text="'hello ' + ${name}">hello! empty</p>
    
    </body>
    </html>

     

     

     


     

    6.3. API

    JSON 데이터 구조 포멧으로 클라이언트에게 데이터를 전달하는 것

    • view.js, react를 사용할 때 api로 데이터만 내려주면 화면은 클라이언트(브라우저)가 알아서 그려지고 정리하는 방식을 사용할때 API방식을 사용한다.
    • 그리고 서버와 서버가 통신할 때 데이터를 주고받을 때 API방식을 사용하기도 함

    • controller에서 @ResponseBody 어노테이션이 붙은 객체는 return값을 http에 그대로 건내준다.
      • 객체가 반환되는 경우에는 디폴트로 json방식으로 변환하여 전달한다.
        ( 변환 라이브러리 : MappingJackson2HttpMessageConverter )
      • 문자일 경우 StringConverter가 처리
        ( 변환 라이브러리 : StringHttpMessageConverter )
    • 클라이언트의 HTTP Accept 헤더와, 서버의 컨트롤러 반환타입 정보
      이 둘을 조합하여 'HttpMessageConveter'가 선택된다.

     

     

    6.3.1. 문자열을 반환하는 방식

    controller

    package hello.hellospring.controller;
    
    ...
    
    @Controller
    public class HelloController {
    
    ...
    
        @GetMapping("hello-string")
        @ResponseBody   // http body부에 이 데이터를 직접 추가한다는 의미 (return값이 그대로 출력되는 특징.)
        public String helloString(@RequestParam("name") String name){
            return "hello " + name; //"hello spring"
        }
    
    }

     

    페이지 소스를 보면 문자열만 나온다.

     

    6.3.2. 객체( json데이터 )로 반환하는 방식

    객체가 json으로 변환되어 브라우저로 전달되는 방식

    controller

    package hello.hellospring.controller;
    
    ...
    
    @Controller
    public class HelloController {
    
    ...
    
        @GetMapping("hello-api")
        @ResponseBody
        public Hello helloApi(@RequestParam("name") String name){
            Hello hello = new Hello();
            hello.setName(name);
            return hello;
        }
    
        // static 객체 생성
        static class Hello{
            private String name;
            
            // 자바빈 표준 규약 (getter/setter, 프로퍼티 접근 방식)
            public String getName() {
                return name;
            }
    
            public void setName(String name) {
                this.name = name;
            }
        }
    
    
    }

    소스보기를 하면 json 방식으로 데이터가 전달되는 것을 확인할 수 있다.

     


     

    7. 회원관리 예제

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

    • 데이터 :
      • 회원id
      • 이름
    • 기능 : 
      • 회원등록
      • 조회
    • 아직 데이터 저장소(DB)가 선정되지 않음 ( 가상의 시나리오 )

     


     

    7.2. 회원 도메인과 리포지토리 생성

    7.2.1. 회원 도메인 생성

    java/hello/hellospring/domain/Member.java

    package hello.hellospring.domain;
    
    public class Member {
    
        private Long id;    // 시스템의 데이터 아이디 (회원 id X)
        private String name;
    
        // getter/setter 
        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;
        }
    }

     

    7.2.2. 레포지토리 생성

    회원 레포지토리 인터페이스 생성
    java/hello/hellospring/repository/MemberRepository.java ( Interface )

    Optional<T> 클래스 : NPE(NullPointerException)을 방지하는 Wrapper 클래스 ( Java 8↑ )
     - 이것을 사용하지 않으면 if문으로 막~ 검사하는 과정을 돌려야 하는 복잡하고 번거로움이 생김

    package hello.hellospring.repository;
    
    import hello.hellospring.domain.Member;
    
    import java.util.List;
    import java.util.Optional;
    
    public interface MemberRepository {
    
        Member save(Member member); //회원 등록
        Optional<Member> findById(Long id); //Id로 회원 조회
        Optional<Member> findByName(String name);   //name으로 회원 조회
        List<Member> findAll(); //전체회원 조회
    
    }

     

    MemoryMemberRepository 클래스 생성
    java/hello/hellospring/repository/MemoryMemberRepository.java

    package hello.hellospring.repository;
    
    import hello.hellospring.domain.Member;
    
    import java.util.*;
    
    public class MemoryMemberRepository implements MemberRepository{
    
        private static Map<Long, Member> store = new HashMap<>();
        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) {
            return Optional.ofNullable(store.get(id));
        }
    
        @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());
        }
    
        // (테스트용) repository를 삭제하는 메서드
        public void clearStore(){
            store.clear();
        }
    }

     


     

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

    개발한 기능을 실행해서 테스트를 진행할 때 main메서드를 통해 실행하거나, 웹 애플리케이션의 컨트롤러를 통해 해당 기능을 실행하기도 하지만, 이러한 방법은 준비-실행하는 과정이 오래걸리고, 반복실행에 어려움이 있으며, 여러 테스트를 한 번에 실행하기 어려운 단점이 있다.
    자바는 JUnit이라는 프레임워크로 테스트를 실행하여 이러한 문제를 해결할 수 있다.

    7.3.1. Test클래스 작성

    java/hello/hellospring/repository/MemoryMemberRepositoryTest.java

    package hello.hellospring.repository;
    
    import hello.hellospring.domain.Member;
    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.*;
    
    class MemoryMemberRepositoryTest {
    
        MemoryMemberRepository repository = new MemoryMemberRepository();
    
        // 하나의 테스트를 진행하고 repository를 비우는 메서드.
        // 테스트 후 repository에 객체가 남아있기 때문에 다음 테스트를 진행할 때 에러가 발생할 수 있다.
        // @AfterEach : 메서드 실행이 끝날 때 마다 한 번씩 동작하는 콜백 메서드
        @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();
            assertThat(member).isEqualTo(result);
            //Assertions.assertEquals(member, result);
        }
    
        @Test
        public void findByName(){
            Member member1 = new Member();
            member1.setName("spring1");
            repository.save(member1);
    
            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);
    
            List<Member> result = repository.findAll();
    
            assertThat(result.size()).isEqualTo(2);
        }
    
    }

     


     

    7.4. 회원 서비스 개발

    repository 클래스는 단순히 데이터를 input/output 하는 역할이라면,
    service 클래스는 네이밍부터 비즈니스에 가깝게 지어지며, 하는 역할도 비즈니스에 가깝게 설계되는 특징이 있다.

    java/hello/hellospring/service/MemberService.java

    package hello.hellospring.service;
    
    import hello.hellospring.domain.Member;
    import hello.hellospring.repository.MemberRepository;
    import hello.hellospring.repository.MemoryMemberRepository;
    
    import java.util.List;
    import java.util.Optional;
    
    public class MemberService {
    
        private final MemberRepository memberRepository = new MemoryMemberRepository();
    
        /**
         * 회원가입
         */
        public Long join(Member member){
            // 같은 이름이 있는 중복 회원X
            validateDuplicateMember(member);    //중복 회원 검증
            memberRepository.save(member);
            return member.getId();
        }
    
        //중복 회원 검증
        private void validateDuplicateMember(Member member) {
            Optional<Member> result = memberRepository.findByName(member.getName());
            // result.ifPresent : 값이 있는지 체크하는 메서드 ( 존재하면 true )
            result.ifPresent( m -> {
                throw new IllegalThreadStateException("이미 존재하는 회원입니다.");
            });
        }
    
    
        /**
         * 전체 회원 조회
         */
        public List<Member> findMembers() {
            return memberRepository.findAll();
        }
    
        /**
         * id로 회원 조회
         */
        public Optional<Member> findOne(Long memberId){
            return memberRepository.findById(memberId);
        }
    }

     


     

    7.5. 회원 서비스 테스트

    7.5.1. 테스트 클래스 생성

    테스트를 하고싶은 클래스 내에서 ' ctrl + shift + T ' 를 입력하면, 아래와 같은 버튼이 등장한다. 
    Create New Test... 를 선택

     

    아래와 같은 창이 나타난다. 
    여러가지 입력을 하고, 테스트 하고싶은 메서드를 선택한뒤 ok를 누르면

     

    이와 같이 자동으로 테스트 클래스가 생성된다.

     

    7.5.2. 테스트 메서드 작성 

    테스트 클래스의 메서드는 해외와 협업하는 경우가 아니라면 한글로 적어도 무방하다.
    빌드될 때 실제 코드에 포함되지 않기 때문에 문제가 없다.

    given - when - then 문법 : 
    어떠한 상황이 주어졌을 때 ( given ),
    이것을 실행하면( when ), 결과는 이렇게 나와야 한다 ( then )

    java/hello/hellospring/service/MemberServiceTest.java

    package hello.hellospring.service;
    
    import hello.hellospring.domain.Member;
    import hello.hellospring.repository.MemoryMemberRepository;
    import org.junit.jupiter.api.AfterEach;
    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.Test;
    
    import static org.assertj.core.api.Assertions.*;
    
    class MemberServiceTest {
    
        MemberService memberService = new MemberService();
        MemoryMemberRepository memberRepository = new MemoryMemberRepository();
        
        @AfterEach
        public void afterEach(){
            memberRepository.clearStore();
        }
    
        @Test
        void 회원가입() {
            // given
            Member member = new Member();
            member.setName("hello");
    
            // when
            Long saveId = memberService.join(member);
    
            // then
            Member findMember = memberService.findOne(saveId).get();
            assertThat(member.getName()).isEqualTo(findMember.getName());
    
        }
    
        @Test
        public void 중복_회원_예외(){
            //given
            Member member1 = new Member();
            member1.setName("spring");
    
            Member member2 = new Member();
            member2.setName("spring");
    
            //when
            memberService.join(member1);
            IllegalThreadStateException e = assertThrows(IllegalThreadStateException.class, () -> memberService.join(member2));
    
            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
            
    /*
            try{
                memberService.join(member2);    // 예외가 발생하여 catch부 실행
                fail();
            } catch(IllegalThreadStateException e){
                // 메시지가 일치하는지 확인
                assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
            }
    */
    
            //then
        }
    
        @Test
        void findMembers() {
        }
    
        @Test
        void findOne() {
        }
    }

     

    7.5.4. 의존 주입 ( DI )

    위와 같은 코드는 MemberService와 MemoryMemberRepository 각각 인스턴스를 생성하기 때문에 데이터가 달라질 수 있다.
    따라서 '의존 주입'을 하여 Service에서도 같은 MemoryMemberRepository가 사용되도록 해보자잉.

     

    java/hello/hellospring/service/MemberService.java

    MemberRepository 객체를 매개변수로 받는 생성자를 만든다.

    package hello.hellospring.service;
    
    import hello.hellospring.domain.Member;
    import hello.hellospring.repository.MemberRepository;
    
    import java.util.List;
    import java.util.Optional;
    
    public class MemberService {
    
        private final MemberRepository memberRepository;
    
        public MemberService(MemberRepository memberRepository) {
            this.memberRepository = memberRepository;
        }
    
    
        /**
         * 회원가입
         */
    
    ...
    
    }

     

    java/hello/hellospring/service/MemberServiceTest.java

    @BeforeEach : 테스트 메서드가 실행되기 전 실행하는 메서드

    따라서 각 메서드가 실행되기 전에 MemoryMemberRepository객체가 생성되고,
    그 인스턴스로 MemberService 객체가 생성되어 그 데이터가 보존?될 수 있다.

    package hello.hellospring.service;
    
    import hello.hellospring.domain.Member;
    import hello.hellospring.repository.MemoryMemberRepository;
    import org.junit.jupiter.api.AfterEach;
    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.Test;
    
    import static org.assertj.core.api.Assertions.*;
    import static org.junit.jupiter.api.Assertions.*;
    
    class MemberServiceTest {
    
        MemberService memberService;
        MemoryMemberRepository memberRepository;
    
        @BeforeEach
        public void beforeEach(){
            memberRepository = new MemoryMemberRepository();
            memberService = new MemberService(memberRepository);
        }
    
        @AfterEach
        public void afterEach(){
            memberRepository.clearStore();
        }
    
        @Test
        void 회원가입() {
            // given
            Member member = new Member();
            member.setName("hello");
    
            // when
            Long saveId = memberService.join(member);
    
            // then
            Member findMember = memberService.findOne(saveId).get();
            assertThat(member.getName()).isEqualTo(findMember.getName());
    
        }
    
    ...
    
    }

     

     


     

    8. 스프링 빈(Spring Bean)과 의존관계

    컨트롤러가 서비스를 통해 데이터에 접근해야 하는데, 이러한 것을 두 객체는 '의존관계'가 있다고 표현한다.

    • 컴포넌트 스캔자동 의존관계 설정
      • @Controller, @Service, @Repository 등 코드에 작성하여 의존관계를 설정하는 방식 : 컴포넌트 스캔
        ( @Component의 특수화된(@Component를 포함) 케이스 )
      • 그리고 @Autowired 어노테이션으로 클래스들의 연관관계를 형성한다.
        ( 객체 생성시점에 스프링 컨테이너에서 해당 빈을 찾아 주입(DI)한다. )
    • 자바 코드로 직접 스프링 빈 등록하기 

     

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

    8.1.1. MemberController.java

    java/hello/hellospring/controller/MemberController.java

    • 클래스 선언부에 @Controller를 사용해 스프링 빈에 등록한다.
    • @Autowired를 MemberService가 매개변수인 생성자에 사용하여 해당 빈을 가져와 사용하도록 한다.
      ( 사용하려면 MemberService가 스프링 빈에 등록되어야 한다 )
    package hello.hellospring.controller;
    
    import hello.hellospring.service.MemberService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    
    @Controller
    public class MemberController {
    
        // private final MemberService memberService = new MemberService();
        private final MemberService memberService;
    
        @Autowired
        public MemberController(MemberService memberService){
            this.memberService = memberService;
        }
        
        ...
        
    }

     

    8.1.2. MemberService.java 수정

    • 클래스 선언부 위에 @Service 애너테이션을 입력하여 스프링 빈으로 등록
    • MemberRepository를 매개변수로 갖는 생성자 위에 @Autowired를 입력하여 의존주입한다.
      ( MemberRepository는 Interface이므로 이를 구현하는 클래스인 MemoryMemberRepository를 빈으로 등록되어 있어야 한다. )
    package hello.hellospring.service;
    
    import hello.hellospring.domain.Member;
    import hello.hellospring.repository.MemberRepository;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import java.util.List;
    import java.util.Optional;
    
    @Service
    public class MemberService {
    
        private final MemberRepository memberRepository;
    
        @Autowired
        public MemberService(MemberRepository memberRepository) {
            this.memberRepository = memberRepository;
        }
    
    ...
    
    }

     

    8.1.3. MemoryMemberRepository.java

    • MemberRepository.java 인터페이스를 구현하는 구현 클래스인 MemoryMemberRepository클래스를
      @Repository를 사용해 스프링 빈으로 등록한다.
    package hello.hellospring.repository;
    
    import hello.hellospring.domain.Member;
    import org.springframework.stereotype.Repository;
    
    import java.util.*;
    
    @Repository
    public class MemoryMemberRepository implements MemberRepository{
    
        private static Map<Long, Member> store = new HashMap<>();
        private static long sequence = 0L;
    
    ...
    
    }

     


     

    8.2. 자바 코드로 직접 스프링 빈 등록하기

    • 회원 서비스와 회원 리포지토리의 @Service, @Repository, @Autowired 애너테이션을 제거하고 진행한다.
    • Controller는 스프링에서 직접 관리하기 때문에 어노테이션을 삭제하지 않는다.

     

    8.2.1. Configuration 클래스 생성

    java/hello/hellospring/SpringConfig.java

    • @Configuration으로 클래스를 감싸 스프링이 빈 등록을 시킬 수 있도록 한다.
    • 필드 메서드에는 @Bean 어노테이션으로 해당 클래스가 스프링 빈에 등록하고,
      내부에는 new 키워드로 의존주입을 한다.( 자동 의존관계 설정에서 @Autowired에 해당 )
    package hello.hellospring;
    
    import hello.hellospring.domain.Member;
    import hello.hellospring.repository.MemberRepository;
    import hello.hellospring.repository.MemoryMemberRepository;
    import hello.hellospring.service.MemberService;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import java.util.List;
    import java.util.Optional;
    
    @Configuration
    public class SpringConfig {
    
        @Bean
        public MemberService memberService(){
            return new MemberService(memberRepository());
        }
    
        @Bean
        public MemberRepository memberRepository(){
            return new MemoryMemberRepository();
        }
        
    }

     

    8.2.2. 의존 주입

    과거에는 XML로 스프링 빈을 설정했지만, 최근에는 잘 사용하지 않는 방법.

     

    의존주입(DI)에는 3가지 방법이 있다.

    • 필드 주입 ( 내가 프로젝트에서 사용한 방법 ) - 별로 안좋다고 함..
    • setter 주입 ( 과거에 주로 사용했지만, 최근에는 사용하지 않는 방식 ) - 멤버필드인 객체를 setter로 생성하여 주입하는 방식 ;  메서드가 public하게 노출되면서 중간에 바꿔치기될 가능성이 비교적 높다.
    • 생성자 주입 - 가장 보편적으로 사용됨

     

    정형화되어있지 않은 상황 ( 차후에 구현 클래스를 변경해야 하는 상황)에서는 자바 코드로 직접 스프링 빈을 등록하는 것이 더 유리하다.
    현재 작성한 프로젝트에서는, DB가 정해지지 않은 상황이다. (MemoryMemberRepository를 변경해야 하는 상황 ) 

     


     

    9. 회원 관리 예제 - 웹 MVC 개발

    9.1. 홈 화면 추가

     

    홈 컨트롤러 추가

    java/hello/hellospring/controller/HomeController.java

    package hello.hellospring.controller;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    
    @Controller
    public class HomeController {
    
        // main페이지(home)
        @GetMapping("/")
        public String home(){
            return "home";
        }
    
    }

     

    회원 관리용 홈

    resources/templates/home.html

    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
    
    <body>
    <!-- home.html -->
    
    <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>

     

    이전에 만들었던 index.html(static)로 이동하지 않는 이유는,
    스프링에 우선순위가 있기 때문이다.

    1. 스프링은 스프링 컨테이너에서 일치하는 관련 컨트롤러를 우선 탐색하고,
    2. 그곳에 없으면 static파일을 탐색한다.

     


     

    9.2. 회원 등록

    9.2.1. 회원 등록 폼 컨트롤러 ( GET )

    java/hello/hellospring/controller/MemberController.java

    package hello.hellospring.controller;
    
    import hello.hellospring.domain.Member;
    import hello.hellospring.service.MemberService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    
    @Controller
    public class MemberController {
    
        // private final MemberService memberService = new MemberService();
        private MemberService memberService;
    
    
        @Autowired
        public MemberController(MemberService memberService){
            this.memberService = memberService;
        }
    
        @GetMapping("members/new")
        public String createForm(){
            return "members/createMemberForm";
        }
    
    ...
    
    }

     

    회원 등록 폼 html

    resources/templates/members/createMemberForm.html

    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
    
    <body>
    <!-- createMemberForm.html -->
    
    <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>

     

     

    9.2.2. 회원 등록 컨트롤러 ( POST )

    java/hello/hellospring/controller/MemberController.java

    package hello.hellospring.controller;
    
    import hello.hellospring.domain.Member;
    import hello.hellospring.service.MemberService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    
    @Controller
    public class MemberController {
    
        // private final MemberService memberService = new MemberService();
        private MemberService memberService;
    
    
        @Autowired
        public MemberController(MemberService memberService){
            this.memberService = memberService;
        }
    
    ...
    
        @PostMapping("members/new")
        public String create(MemberForm form){
            Member member = new Member();
            member.setName(form.getName());
    
            memberService.join(member);
    
            return "redirect:/";
        }
        
    }

    아직은 회원 등록을 해도 확인이 불가능하다.
    다음 단계인 회원 조회를 통해 확인이 가능하다.

     


     

    9.3. 회원 조회

    9.3.1. 전체 회원 조회 컨트롤러

    java/hello/hellospring/controller/MemberController.java

    package hello.hellospring.controller;
    
    import hello.hellospring.domain.Member;
    import hello.hellospring.service.MemberService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    
    import java.util.List;
    
    @Controller
    public class MemberController {
    
        // private final MemberService memberService = new MemberService();
        private MemberService memberService;
    
    
        @Autowired
        public MemberController(MemberService memberService){
            this.memberService = memberService;
        }
    
    ...
    
        @GetMapping("members")
        public String list(Model model){
            List<Member> members = memberService.findMembers();
            model.addAttribute("members", members);
            return "members/memberList";
        }
    
    }

     

    9.3.2. 회원 리스트 html

    resources/templates/members/memberList.html

    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
    
    <body>
    <!-- memberList.html -->
    <div class="container">
        <div>
            <table>
                <thead>
                <tr>
                    <th>#</th>
                    <th>이름</th>
                </tr>
                </thead>
                <tbody>
                <tr th:each="member : ${members}">
                    <td th:text="${member.id}"></td>
                    <td th:text="${member.name}"></td>
                </tr>
                </tbody>
            </table>
        </div>
    </div><!-- /container -->
    
    </body>
    </html>

     


     

    10. 스프링 DB 접근 기술

    10.1. H2 데이터베이스 설치

    10.1.1. H2 DB 세팅

    설치 후 db파일을 생성한 후, 소캣 접근을 해야한다.

    계속해서 파일접근을 하면 애플리케이션과 웹콘솔과 동시접근으로 오류가 발생할 수 있기 때문이다!!

    일단 이렇게 실행시키면 아래와 같이 데이터베이스 ( test라는 이름의 )파일이 생성된다.
    c:user/pc명에 생김
    db파일이 생성된것을 확인하면 이후에는 소캣접근을 한다.

     

    10.1.2. member테이블 생성

    • bigint : Java의 Long 타입은 h2에서 bigint라는 타입,
    • generated by default as identity : 값이 null로 insert되면, 자동으로 값을 채워진다.
    drop table if exists member CASCADE;
    create table member
    (
     id bigint generated by default as identity,
     name varchar(255),
     primary key(id)
    );

     

     


     

    10.2. 순수 JDBC

    10.2.1. 환경설정

    build.gradle

    jdbc와 H2 DB에 관련된 라이브러리를 추가한다.

    ...
    
    dependencies {
    
    	...
    
    	/* jdbc driver */
    	implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    	runtimeOnly 'com.h2database:h2'
    }
    	
    ...
    
    }

     

    resources/application.properties

    H2 DB에 접근하기 위한 DB설정 정보를 작성한다.

    ...
    
    spring.datasource.url=jdbc:h2:tcp://localhost/~/test
    spring.datasource.driver-class-name=org.h2.Driver
    spring.datasource.username=sa

     

    10.2.2. Jdbc 리포지토리 구현

    java/hello/hellospring/repository/JdbcMemberRepository.java

    • 회원을 db에 저장하는 repository
    • 앞에서 만들었던 MemoryMemberRepository는 메모리에 저장하는 repository라는 차이가 있다.
    package hello.hellospring.repository;
    
    import hello.hellospring.domain.Member;
    import org.springframework.jdbc.datasource.DataSourceUtils;
    
    import javax.sql.DataSource;
    import java.sql.*;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Optional;
    
    public class JdbcMemberRepository implements MemberRepository{
        // 회원을 db에 저장하는 repository
        // 앞에서 만들었던 MemoryMemberRepository는 메모리에 저장하는 repository라는 차이가 있다.
    
        private final DataSource dataSource;
    
        public JdbcMemberRepository(DataSource dataSource) {
            this.dataSource = dataSource;
        }
    
        @Override
        public Member save(Member member) {
            String sql = "insert into member(name) values(?)";
    
            Connection conn = null;
            PreparedStatement pstmt = null;
            ResultSet rs = null;
    
            try {
                conn = getConnection();
                pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
    
                pstmt.setString(1, member.getName());
    
                pstmt.executeUpdate();
                rs = pstmt.getGeneratedKeys();
    
                if (rs.next()) {
                    member.setId(rs.getLong(1));
                } else {
                    throw new SQLException("id 조회 실패");
                }
                return member;
            } catch (Exception e) {
                throw new IllegalStateException(e);
            } finally {
                close(conn, pstmt, rs);
            }
        }
        
        @Override
        public Optional<Member> findById(Long id) {
            String sql = "select * from member where id = ?";
            Connection conn = null;
            PreparedStatement pstmt = null;
            ResultSet rs = null;
    
            try {
                conn = getConnection();
                pstmt = conn.prepareStatement(sql);
                pstmt.setLong(1, id);
                rs = pstmt.executeQuery();
                if(rs.next()) {
                    Member member = new Member();
                    member.setId(rs.getLong("id"));
                    member.setName(rs.getString("name"));
                    return Optional.of(member);
                } else {
                    return Optional.empty();
                }
            } catch (Exception e) {
                throw new IllegalStateException(e);
            } finally {
                close(conn, pstmt, rs);
            }
        }
        
        @Override
        public List<Member> findAll() {
            String sql = "select * from member";
            Connection conn = null;
            PreparedStatement pstmt = null;
            ResultSet rs = null;
            try {
                conn = getConnection();
                pstmt = conn.prepareStatement(sql);
                rs = pstmt.executeQuery();
                List<Member> members = new ArrayList<>();
                while(rs.next()) {
                    Member member = new Member();
                    member.setId(rs.getLong("id"));
                    member.setName(rs.getString("name"));
                    members.add(member);
                }
                return members;
            } catch (Exception e) {
                throw new IllegalStateException(e);
            } finally {
                close(conn, pstmt, rs);
            }
        }
        
        @Override
        public Optional<Member> findByName(String name) {
            String sql = "select * from member where name = ?";
            Connection conn = null;
            PreparedStatement pstmt = null;
            ResultSet rs = null;
            try {
                conn = getConnection();
                pstmt = conn.prepareStatement(sql);
                pstmt.setString(1, name);
                rs = pstmt.executeQuery();
                if(rs.next()) {
                    Member member = new Member();
                    member.setId(rs.getLong("id"));
                    member.setName(rs.getString("name"));
                    return Optional.of(member);
                }
                return Optional.empty();
            } catch (Exception e) {
                throw new IllegalStateException(e);
            } finally {
                close(conn, pstmt, rs);
            }
        }
        private Connection getConnection() {
            return DataSourceUtils.getConnection(dataSource);
        }
        private void close(Connection conn, PreparedStatement pstmt, ResultSet rs)
        {
            try {
                if (rs != null) {
                    rs.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
            try {
                if (pstmt != null) {
                    pstmt.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
            try {
                if (conn != null) {
                    close(conn);
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        private void close(Connection conn) throws SQLException {
            DataSourceUtils.releaseConnection(conn, dataSource);
        }
    }​

     

    java/hello/hellospring/SpringConfig.java

    기존 MemoryMemberRepository에 연결되어 있던 bean을 JdbcMemberRepository로 변경하고,
    JdbcMemberRepository는 DataSource를 전달인수로 필요로 하므로, DataSource를 생성자 주입 방식으로 Bean을 생성하여 주입한다.

    package hello.hellospring;
    
    import hello.hellospring.domain.Member;
    import hello.hellospring.repository.JdbcMemberRepository;
    import hello.hellospring.repository.MemberRepository;
    import hello.hellospring.repository.MemoryMemberRepository;
    import hello.hellospring.service.MemberService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import javax.sql.DataSource;
    import javax.xml.crypto.Data;
    import java.util.List;
    import java.util.Optional;
    
    @Configuration
    public class SpringConfig {
    
        private DataSource dataSource;
    
        @Autowired
        public SpringConfig(DataSource dataSource) {
            this.dataSource = dataSource;
        }
    
        ...
    
        @Bean
        public MemberRepository memberRepository(){
            // return new MemoryMemberRepository();
            return new JdbcMemberRepository(dataSource);
        }
        
    }

    • 이와 같은 원칙은 'SOLID 설계 원칙'에서 '개방-폐쇄 원칙(OCP; Open-Close Principle)'이라 한다.
      • 확장에는 열려있고, 수정,변경에는 닫힌 구조.
      • 스프링의 DI를 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있다.

     

     


     

    10.3. 스프링 통합 테스트

    앞서 만들었던 테스트는 스프링과 연관이 없는 순수한 자바 코드로 작성된 테스트였음.

    이제 스프링과 엮어서 테스트를 진행해 본다.

     

    java/hello/hellospring/service/MemberServiceIntegrationTest.java

    기존 MemberServiceTest를 복제하여 네이밍을 바꾸고 수정한다.

    package hello.hellospring.service;
    
    import hello.hellospring.domain.Member;
    import hello.hellospring.repository.MemberRepository;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.transaction.annotation.Transactional;
    
    import static org.assertj.core.api.Assertions.assertThat;
    import static org.junit.jupiter.api.Assertions.assertThrows;
    
    @SpringBootTest
    @Transactional
    class MemberServiceIntegrationTest {
    
        @Autowired
        MemberService memberService;
        
        @Autowired
        MemberRepository memberRepository;
    
        @Test
        void 회원가입() {
            // given
            Member member = new Member();
            member.setName("hello");
    
            // when
            Long saveId = memberService.join(member);
    
            // then
            Member findMember = memberService.findOne(saveId).get();
            assertThat(member.getName()).isEqualTo(findMember.getName());
    
        }
    
        @Test
        public void 중복_회원_예외(){
            //given
            Member member1 = new Member();
            member1.setName("spring");
    
            Member member2 = new Member();
            member2.setName("spring");
    
            //when
            memberService.join(member1);
            IllegalThreadStateException e = assertThrows(IllegalThreadStateException.class, () -> memberService.join(member2));
    
            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
        }
    }
    • @SpringBootTest : 스프링부트를 구동하여 테스트를 진행한다.
    • @Transactional : 테스트 케이스에 @Transactional이 있다면, 테스트 직전에 트랜잭션을 하고, 하나의 테스트가 끝날 때 마다 롤백을 한다.
      이렇게 하면, 매 테스트시 DB에 데이터가 보존되므로, 다음 테스트에 영향을 주지 않는다.

     


     

    10.4. 스프링 JdbcTemplate

    10.4.1. 환경설정

    순수 JDBC와 동일한 환경설정을 한다.

    스프링 JdbcTemplate와 MyBatis같은 라이브러리는,
    JDBC API에서 본 반복되는 코드를 대부분 제거했으나, SQL은 직접 작성해야 한다.

     

    10.4.2. JdbcTemplate 리포지토리 구현

    java/hello/hellospring/repository/JdbcTemplateMemberRepository.java

    package hello.hellospring.repository;
    
    
    import hello.hellospring.domain.Member;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.jdbc.core.RowMapper;
    import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
    import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
    
    import javax.sql.DataSource;
    import javax.xml.crypto.Data;
    import java.sql.ResultSet;
    import java.sql.SQLException;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.Optional;
    
    public class JdbcTemplateMemberRepository implements MemberRepository {
    
        private final JdbcTemplate jdbcTemplate;
    
        @Autowired
        public JdbcTemplateMemberRepository(DataSource dataSource) {
            jdbcTemplate = new JdbcTemplate(dataSource);
        }
    
        @Override
        public Member save(Member member) {
            SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
            jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");
    
            Map<String, Object> parameters = new HashMap<>();
            parameters.put("name", member.getName());
    
            Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
            member.setId(key.longValue());
            return member;
    
        }
    
        @Override
        public Optional<Member> findById(Long id) {
            List<Member> result = jdbcTemplate.query("select * from member where id=?", memberRowMapper(), id);
            return result.stream().findAny();
        }
    
        @Override
        public Optional<Member> findByName(String name) {
            List<Member> result = jdbcTemplate.query("select * from member where name=?", memberRowMapper(), name);
            return result.stream().findAny();
        }
    
        @Override
        public List<Member> findAll() {
            return jdbcTemplate.query("select * from member", memberRowMapper());
        }
    
        private RowMapper<Member> memberRowMapper(){
    //        return new RowMapper<Member>() {
    //            @Override
    //            public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
    //                Member member = new Member();
    //                member.setId(rs.getLong("id"));
    //                member.setName(rs.getString("name"));
    //                return member;
    //            }
    //        };
    
            return (rs, rowNum) -> {
                Member member = new Member();
                member.setId(rs.getLong("id"));
                member.setName(rs.getString("name"));
                return member;
            };
        }
    }

     

    java/hello/hellospring/SpringConfig.java

    기존 JdbcMemberRepository로 생성되던 빈을 JdbcTemplateMemberRepository로 변경하고, DataSource를 주입한다.

    package hello.hellospring;
    
    import hello.hellospring.domain.Member;
    import hello.hellospring.repository.JdbcMemberRepository;
    import hello.hellospring.repository.JdbcTemplateMemberRepository;
    import hello.hellospring.repository.MemberRepository;
    import hello.hellospring.repository.MemoryMemberRepository;
    import hello.hellospring.service.MemberService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import javax.sql.DataSource;
    import javax.xml.crypto.Data;
    import java.util.List;
    import java.util.Optional;
    
    @Configuration
    public class SpringConfig {
    
        private DataSource dataSource;
    
        @Autowired
        public SpringConfig(DataSource dataSource) {
            this.dataSource = dataSource;
        }
    
        ...
    
        @Bean
        public MemberRepository memberRepository(){
            // return new MemoryMemberRepository();
            // return new JdbcMemberRepository(dataSource);
            return new JdbcTemplateMemberRepository(dataSource);
        }
    }

     


     

    10.5. JPA

    • JPA는 반복되는 코드는 물론, 기본적인 SQL도 JPA가 직접 만들어 실행.
    • JPA를 사용하면, SQL과 데이터 중심의 설계 -> 객체 중심의 설계로 패러다임을 전환이 가능하다.
    • JPA를 사용하면 개발 생산성을 크게 높일 수 있다.
    • MyBatis는 중국과 우리나라에서 많이 사용되지만,
      최근 우리나라에서도 점차적으로 JPA의 점유율이 증가하고 있다. 

     

    10.5.1. JPA 환경설정

    build.gradle

    jpa라이브러리는 jdbc 관련 라이브러리를 포함한다.

    ...
    
    dependencies {
    	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    	implementation 'org.springframework.boot:spring-boot-starter-web'
    	testImplementation 'org.springframework.boot:spring-boot-starter-test'
    
    	/* jdbc driver */
    //	implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    
    	runtimeOnly 'com.h2database:h2'
    
    	/* jpa */
    	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    
    }
    
    ...

     

    resources/application.properties

    • show-sql  : JPA가 생성하는 SQL을 출력한다.
    • ddl-auto : JPA는 테이블을 자동으로 생성하는 기능을 제공하는데 none을 입력하면 해당 기능을 꺼놓는다.
      • create를 입력하면 엔티티 정보를 바탕으로 테이블도 직접 생성한다.
    server.port=8090
    
    spring.datasource.url=jdbc:h2:tcp://localhost/~/test
    spring.datasource.driver-class-name=org.h2.Driver
    spring.datasource.username=sa
    
    spring.jpa.show-sql=true
    spring.jpa.hibernate.ddl-auto=none

     

    10.5.2. JPA 엔티티 매핑

    JPA는 인터페이스만 제공되고, 구현체로는 Hibernate, EclipseLink등 여러 구현기술들이 존재한다.
    (우리는 JPA와 Hibernate를 사용)
    쉽게 말해서, JPA는 표준 인터페이스이며, JPA의 구현은 여러 업체들의 라이브러리를 사용하는 것

    JPA를 사용하려면 우선 Entity를 매핑해야한다.
    @Entity 어노테이션을 입력하게되면, 이제 JPA가 관리하는 클래스가 되는 것.
    JPA를 사용하여 테이블과 매핑할 클래스반드시 @Entity가 명시되어야 한다.

    그리고 그 Entity 요소 중 'PK'(Primary Key)와 '값 자동생성'를 매핑해야한다.

    • @Id : pk매핑
    • @GeneratedValue(strategy = Generation.IDENTITY) : 값이 자동 생성되는 Column이므로 자동생성임을 명시한다.
      자동생성 키워드의 종류는, IDENTITY, SEQUENCE, TABLE, AUTO등이 있으며,
      여기서는 기본 키 생성을 데이터베이스에 위임하고, MySQL에서 많이 사용하는 'IDENTITY'를 사용한다.
    package hello.hellospring.domain;
    
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;
    
    @Entity
    public class Member {
    
        @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;    // 시스템의 데이터 아이디 (회원 id X)
        private String name;
    
        // getter/setter
    
    	...
    
    }

     

    10.5.3. JPA 회원 리포지토리

    JPA는 EntityManager 객체로 모든 동작을 한다.
    JPA 라이브러리를 받으면 스프링부트는 자동으로 EntityManager를 생성하므로,
    Repository에서는 그것을 Injection받고 사용하기만 하면 된다.

    특이하게 findByName()과 findAll()에서는 객체지향쿼리(JPQL)을 사용하는데,
    그 이유는 여러 개의 리스트를 조회할 때 pk가 아닌 값으로 검색, 조회하기 때문!! (결과가 복수개)

    ( 그러나, 다음 챕터에서 진행하는 '스프링 데이터 JPA'를 사용하면 객체지향쿼리도 생략이 가능하다... )

    package hello.hellospring.repository;
    
    import hello.hellospring.domain.Member;
    import org.springframework.beans.factory.annotation.Autowired;
    
    import javax.persistence.EntityManager;
    import java.util.List;
    import java.util.Optional;
    
    public class JpaMemberRepository implements MemberRepository{
    
        private final EntityManager em;
    
        @Autowired
        public JpaMemberRepository(EntityManager em) {
            this.em = em;
        }
    
        @Override
        public Member save(Member member) {
            em.persist(member);
            return member;
        }
    
        @Override
        public Optional<Member> findById(Long id) {
            Member member = em.find(Member.class, id);
            return Optional.ofNullable(member);
        }
    
        @Override
        public Optional<Member> findByName(String name) {
            List<Member> result = em.createQuery("select m from Member m where m.name =: name", Member.class)
                    .setParameter("name", name)
                    .getResultList();
    
            return result.stream().findAny();
        }
    
        @Override
        public List<Member> findAll() {
            return em.createQuery("select m from member m", Member.class)
                    .getResultList();
        }
        
    }

     

    10.5.4. 서비스 계층에 트랜잭션 추가

    JPA로 데이터를 저장, 변경할 때 트랜잭션이 필요하다.

    • @Transactional을 입력한다. : JPA를 통한 모든 데이터 변경은 반드시 트랜잭션 안에서 실행되어야 한다.
      • 스프링은 해당 클래스의 메서드를 실행할 때 트랜잭션을 시작하고, 메서드 정상 종료되면 트랜잭션을 커밋한다.
      • 만약 런타임 예외가 발생하면 롤백한다.
    package hello.hellospring.service;
    
    import hello.hellospring.domain.Member;
    import hello.hellospring.repository.MemberRepository;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    import java.util.List;
    import java.util.Optional;
    
    @Transactional
    public class MemberService {
    
        private final MemberRepository memberRepository;
    
        public MemberService(MemberRepository memberRepository) {
            this.memberRepository = memberRepository;
        }
    
    
        /**
         * 회원가입
         */
    
    ...
    
    }

     

    10.5.5. JPA를 사용하도록 스프링 설정 변경

    package hello.hellospring;
    
    import hello.hellospring.repository.*;
    import hello.hellospring.service.MemberService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import javax.persistence.EntityManager;
    
    @Configuration
    public class SpringConfig {
    
        private EntityManager em;
    
        @Autowired
        public SpringConfig(EntityManager em) {
            this.em = em;
        }
    
        ...
    
        @Bean
        public MemberRepository memberRepository(){
            // return new MemoryMemberRepository();
            // return new JdbcMemberRepository(dataSource);
            // return new JdbcTemplateMemberRepository(dataSource);
            return new JpaMemberRepository(em);
        }
        
    }

     


     

    10.6. 스프링 데이터 JPA

    '스프링 부트'와 'JPA'만 사용해도 개발 생산성이 눈에 띄게 증가하고, 코드 줄도 확연히 줄어든다.
    여기에 '스프링 데이터 JPA'를 더하면, 마치 마법처럼 리포지토리에 구현 클래스 없이 인터페이스만으로 개발을 완료할 수 있게 된다.

    그리고 반복해서 개발해오던 CRUD기능도 '스프링 데이터 JPA'가 모두 제공된다.

    지금까지 조금이라도 단순하며 반복됬던 개발 코드들이 확연히 줄어든다.
    따라서 개발자는 핵심 비즈니스 로직을 개발하는데에 더욱 집중할 수 있는 환경이 마련된다.

    그러나, '스프링 데이터 JPA'는 'JPA'를 편리하게 사용하는 기술이므로,
    'JPA'를 먼저 학습한 뒤에 '스프링 데이터 JPA'를 학습해야 한다.

     

    10.6.1. 스프링 데이터 JPA 회원 리포지토리 생성 ( interface )

    package hello.hellospring.repository;
    
    import hello.hellospring.domain.Member;
    import org.springframework.data.jpa.repository.JpaRepository;
    
    import java.util.Optional;
    
    public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
    
        @Override
        Optional<Member> findByName(String name);
        // select m  from Member m where m.name = ? 형태로 스프링 데이터 JPA가 JPQL을 짬
    }

     

    10.6.2. 스프링 데이터 JPA 회원 리포지토리를 사용하도록 설정 변경

    스프링 데이터 JPA가 SpringDataJpaMemberRepository를 스프링 빈으로 자동 등록한다.
    따라서 MemberRepository를 final로 생성하여 생성자 주입 되도록 구성하고, 서비스에 주입한다.

    package hello.hellospring;
    
    import hello.hellospring.repository.*;
    import hello.hellospring.service.MemberService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class SpringConfig {
    
        private final MemberRepository memberRepository;
    
        @Autowired
        public SpringConfig(MemberRepository memberRepository) {
            this.memberRepository = memberRepository;
        }
    
    //    private EntityManager em;
    //
    //    @Autowired
    //    public SpringConfig(EntityManager em) {
    //        this.em = em;
    //    }
    
        @Bean
        public MemberService memberService(){
            return new MemberService(memberRepository);
        }
    
    //    @Bean
    //    public MemberRepository memberRepository(){
    //        return new MemoryMemberRepository();
    //        return new JdbcMemberRepository(dataSource);
    //        return new JdbcTemplateMemberRepository(dataSource);
    //        return new JpaMemberRepository(em);
    //    }
    }

     

    10.6.3. 스프링 데이터 JPA가 제공하는 클래스

    각 클래스를 살펴보면 메서드들이 CRUD / 단순조회가 기본적으로 제공되어진다. 

     

    10.6.4. 스프링 데이터 JPA가 제공하는 기능

     

    10.6.5. 참고 : 

    실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용하고, 복잡한 동적쿼리는 'Querydsl'이라는 라이브러리를 사용하면 된다.
    'Querydsl'을 사용하면 쿼리도 자바 코드로 안전하게 작성 가능하며, 동적 쿼리도 편리하게 작성할 수 있다.
    이 조합으로 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리를 사용하거나, 앞서 학습한 JdbcTemplate을 사용하면 된다. ( 스프링 데이터 JPA와 섞어서 사용이 가능하다. )

     


     

    11. AOP

    • AOP : Aspect Oriented Programming

    11.1. AOP가 필요한 상황

    • 모든 메서드의 호출 시간을 측정하고 싶을 때
    • 공통 관심사항(cross-cutting concern) vs 핵심 관심 사항(core concern)
    • 회원 가입 시간, 회원 조회 시간을 측정이 필요할 때

     

    11.1.1. 시간측정 기능 추가하기 ( AOP적용 X )

    package hello.hellospring.service;
    
    ...
    
    @Transactional
    public class MemberService {
    
        private final MemberRepository memberRepository;
    
        public MemberService(MemberRepository memberRepository) {
            this.memberRepository = memberRepository;
        }
    
    
        /**
         * 회원가입
         */
        public Long join(Member member){
    
            long start = System.currentTimeMillis();
    
            try {
                // 같은 이름이 있는 중복 회원X
                validateDuplicateMember(member);    //중복 회원 검증
                memberRepository.save(member);
                return member.getId();
            } finally {
                long finish = System.currentTimeMillis();
                long timeMs = finish-start;
                System.out.println("join = " + timeMs + "ms");
            }
        }
    
        ...
    
        /**
         * 전체 회원 조회
         */
        public List<Member> findMembers() {
        
            long start = System.currentTimeMillis();
    
            try {
                return memberRepository.findAll();
            } finally {
                long finish = System.currentTimeMillis();
                long timeMs = finish-start;
                System.out.println("findMembers = " + timeMs + "ms");
            }
        }
    
        ...
    }

    시간 측정 로직을 각 메서드마다 붙였다...

    문제점 : 

    • 회원 가입, 회원 조회의 '시간을 측정하는 기능'은 '핵심 관심사항'이 아니다.
    • '시간을 측정하는 로직'은 '공통 관심사항'이다.
    • '시간을 측정하는 로직'과 '핵심 비즈니스 로직'이 섞여 유지보수가 어렵다.
    • '시간을 측정하는 로직'을 별도의 공통 로직으로 만들기 매우 어렵다.
      ( 핵심로직의 시작과 끝 부분에 물려야 하기 때문 )
    • '시간을 측정하는 로직'을 변경할 때 모든 로직을 찾아 변경해야 한다.

     


     

    11.2. AOP 적용

    • AOP : Aspect Oriented Programming
    • 공통 관심 사항(cross-cutting concern)과 핵심 관심 사항(core concern)의 분리

     

    11.2.1. 시간 측정 AOP 등록

    java/hello/hellospring/aop/TimeTraceAop.java

    • @Aspect 어노테이션
    • @Component로 빈으로 등록한다.
    • @Around("execution(* hello.hellospring..*(..))") : 
      AOP가 적용될 디렉토리를 설정하는 것
    • joinPoint.proceed(); : 
      Around에서 기준점이 된다.
      • proceed() 전 : aop가 적용된 메서드 호출 전 동작
        proceed() 후 : aop가 적용된 메서드 호출 후 동작 
    package hello.hellospring.aop;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.springframework.stereotype.Component;
    
    @Aspect
    @Component
    public class TimeTraceAop {
    
        @Around("execution(* hello.hellospring..*(..))")
        public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
            long start = System.currentTimeMillis();
            System.out.println("START : " + joinPoint.toString());
            try {
                return joinPoint.proceed();
            }finally {
                long finish = System.currentTimeMillis();
                long timeMs = finish - start;
    
                System.out.println("END : " + joinPoint.toString() + " " +timeMs + "ms");
            }
            
        }
    }

     

    문제 해결 : 

    • 회원가입, 회원 조회 등 '핵심 관심 사항'과 시간을 측정하는 '공통 관심 사항'을 분리한다.
    • 시간을 측정하는 로직을 별도의 공통 로직으로 만들었다.
    • 핵심 관심 사항을 깔끔하게 유지할 수 있다.
    • 변경이 필요하면 이 로직만 변경하면 된다.
    • 원하는 적용 대상을 선택할 수 있다.

     

    11.2.2. 스프링의 AOP 동작 방식 설명

    AOP 적용 전 의존관계

     

    AOP 적용 후 의존 관계

    memberService가 복제된 프록시객체가 주입되고, AOP 실행 되고,  joinPoint.Proceed()가 되면, 실제 memberService가 호출된다.

    memberService가 프록시로 복제되는데 이것이 memberController에 주입된다.

    메서드가 실행되면 프록시memberService가 호출되어 AOP가 실행되고 joinPoint.proceed()가 되면, 실제 memberService가 호출되는 구조.

     

    AOP 적용 전 전체 구성

     

    AOP 적용 후 전체 구성

    반응형