[Spring] 스프링 입문 강의 정리

eunbi·2022년 8월 14일
0

스프링 입문 강의 정리

Framework : 스프링부트 2.6.11
templete, View : 타임리프
환경 : gradel
DB : 로컬 & H2 1.4.200
JDK : 11

Springboot 설치와 초기 설정, 빌드

1. springboot 초기 설정

: Spring Initializr 으로 쉽게 설정할 수 있다.


(2.7.2로 진행하다가 2.7 버전은 인텔리제이에서 제대로 안 된다고 해서 2.6.10으로 진행했다.)

  • 파일을 다운 받고, 압축을 푼 뒤 인텔리제이로 해당 폴더를 오픈한다.

    : 인텔리제이 환경설정을 확인한다. 프로젝트 JDK를 11버전으로 gradle의 run, testing을 인텔리제이 idea로 설정. JVM을 11버전으로 설정한다.

  • HelloController.java 를 run하면 바로 톰캣서버로 올리기 가능.

2. Index page, hello page 만들기

  • 해당 파일은 static 폴더 안에 있으므로 해당 페이지는 바뀌지 않는다. (자동으로 welcome page를 잡아줌)
// path : resources/static/index.html
<!DOCTYPE HTML>
<html>
<head>
    <title>Hello</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
Hello
<a href="/hello">hello</a>
</body>
</html>
  • thymeleaf 템플릿. url접속시 …/hello로 진입하면 “hello” 매핑된 메소드가 호출된다.
// path : java/com.led.hellospring.controller/HelloController.java
package com.led.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";
    }
}
  • 매핑된 controller 메소드에서 반환된 문자를 viewResolver가 찾아서 (ex. “hello!!”) 해당 파일을 찾은 뒤, 처리한다.
// path : resources/templates/hello.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Hello</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p th:text="'안녕하세요. ' + ${data}" >안녕하세요. 손님</p>
</body>
</html>

3. 빌드하기

  1. ./gradlew build
  2. cd build/libs
  3. java -jar hello-spring-0.0.1-SNAPSHOT.jar → 실행됨!
  • 서버 재시작 없이 바로바로 업데이트 하는법. spring-boot-devtools 추가.
    ```bash
    // build.gradle에 의존성 추가.
    dependencies {
    	developmentOnly 'org.springframework.boot:spring-boot-devtools'
    }
    ```
    
    - 자동적으로 컴파일하거나, 수동으로 build > recompile 하면 완료.

웹스프링 웹 개발 기초

1. 정적 컨텐츠

  • /static 에 html 파일을 그대로 넣고 도메인에 해당 파일 링크를 넣어 실행할 경우.

2. MVC(Model, View, Controller)와 템플릿 엔진

  1. Controlller
    // java/com.led.hellospring.controller/HelloController.java
    		@GetMapping("hello-mvc") // 1. MVC 방식
        public String helloMvc(@RequestParam("name") String name, Model model) {
            model.addAttribute("name", name);
            return "hello-template";
        }
  1. View
    // resources/template/hello-template.html
    <html xmlns:th="http://www.thymeleaf.org">
    <body>
    <p th:text="'hello ' + ${name}">hello! empty</p>
    </body>
    </html>
  • 실행 : http://localhost:8080/hello-mvc?name=value

3. API

  • @ResponseBody : viewResolver(뷰 리졸버)를 사용하지 않고, http response body에 직접 반환하게 됨.

    - viewResolver 대신 httpMessageConverter가 동작하여, 객체를 전달하거나 문자를 전달한다.
  1. ResponseBody 문자 반환 : 문자 그대로를 날려준다.

    	@GetMapping("hello-string") // 2. API 방식 (문자 냅다 반환)
        @ResponseBody
        public String helloString(@RequestParam("name") String name) {
            return "hello " + name;
        }

    실행 : http://localhost:8080/hello-string?name=value

  1. ResponseBody 객체 반환 : json형식으로 객체를 날려준다.

    	@GetMapping("hello-api") // 2. API 방식 (객체 반환)
        @ResponseBody
        public Hello helloApi(@RequestParam("name") String name) {
            Hello hello = new Hello();
            hello.setName(name);
            return hello;
        }
    
        static class Hello {
            private String name;
    
            public String getName() {
                return name;
            }
    
            public void setName(String name) {
                this.name = name;
            }
        }
    • 실행 : http://localhost:8080/hello-api?name=spring

회원 관리 예제 - 백엔드 개발

1. 비즈니스 요구사항

데이터: 회원ID, 이름
기능: 회원 등록, 조회
아직 데이터 저장소가 선정되지 않음(가상의 시나리오)
-> 인터페이스로 구현 클래스를 변경할 수 있도록 설계한다. 구현체로 가벼운 메모리 기반의 데이터 저장소 사용
[MemberService]->[(interface)MemberRepository]<-[MemoryMemberRepository]

2. 회원 도메인, 리포지토리 - 테스트 케이스까지

  • 회원 객체 : id와 name을 가진다.
    package com.led.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;
        }
    }
  • 회원 리포지토리 인터페이스
    • save : 멤버를 세이브

    • findById : 해당 id를 가진 멤버를 검색

    • findByName : 해당 name을 가진 멤버를 검색

      ❕ Optional : Null이 나올 수 있는 값일 경우, Optional을 이용하여 처리할 수 있다.
      package com.led.hellospring.repository;
      
      import com.led.hellospring.domain.Member;
      
      import java.util.List;
      import java.util.Optional;
      
      public interface MemberRepository {
          Member save(Member member);
          Optional<Member> findById(Long id);
          Optional<Member> findByName(String name);
          List<Member> findAll();
      }
  • 회원 리포지토리 메모리 구현체
    • 리포지토리는 map 구조로 구현된다. (ID : member)

      package com.led.hellospring.repository;
      
      import com.led.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());
          }
      
          public void clearStore() {
              store.clear();
          }
      }
  • 구현 테스트 케이스 작성
    package com.led.hellospring.repository;
    
    import com.led.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 static org.assertj.core.api.Assertions.*;
    
    class MemoryMemberRepositoryTest {
    
        MemoryMemberRepository repository = new MemoryMemberRepository();
    
        @AfterEach
        public void afterEach() {   // 테스트가 순서 의존적으로 작동하지 않도록, 각각 테스트의 끝마다 repository 변수를 비워주어야 함.
            repository.clearStore();
        }
    
        @Test
        public void save() {
            Member member = new Member();
            member.setName("snuggle");
    
            repository.save(member);
    
            Member result = repository.findById(member.getId()).get();
            //Assertions.assertEquals(member, result);
            assertThat(member).isEqualTo(result);
        }
    
        @Test
        public void findByName() {
            Member member1 = new Member();
            member1.setName("nuggle1");
            repository.save(member1);
    
            Member member2 = new Member();
            member2.setName("nuggle2");
            repository.save(member2);
    
            Member result = repository.findByName("nuggle1").get();
    
            assertThat(result).isEqualTo(member1);
        }
    
        @Test
        public void findAll() {
            Member member1 = new Member();
            member1.setName("nuggle1");
            repository.save(member1);
    
            Member member2 = new Member();
            member2.setName("nuggle2");
            repository.save(member2);
    
            List<Member> result = repository.findAll();
    
            assertThat(result.size()).isEqualTo(2);
        }
    }

3. 회원 서비스 - 테스트 케이스까지

  • 회원 서비스 개발
    • join : 회원 가입 메소드. 회원 이름이 중복되지는 않았는지 확인하고, 리포지토리에 저장한다.

    • validateDuplicateMember : 해당 회원 이름이 리포지토리에 있는지 확인하는 메소드.

    • findMembers : 전체 회원 조회 메소드

    • findOne : 해당 멤버 id를 지니는 멤버를 찾는 메소드

      package com.led.hellospring.service;
      
      import com.led.hellospring.domain.Member;
      import com.led.hellospring.repository.MemberRepository;
      import com.led.hellospring.repository.MemoryMemberRepository;
      
      import java.util.List;
      import java.util.Optional;
      
      public class MemberService {
          private final MemberRepository memberRepository; // = new MemoryMemberRepository();
      
          public MemberService(MemberRepository memberRepository) {   // test, DI (dependency injection)
              this.memberRepository = memberRepository;
          }
      
          /**
           * 회원 가입
           */
          public Long join (Member member) {
              /* 회원 이름 중복 확인 */
              validateDuplicateMember(member);
      
              memberRepository.save(member);
              return member.getId();
          }
      
          private void validateDuplicateMember(Member member) {
              //        Optional<Member> result = memberRepository.findByName(member.getName());
              //        result.ifPresent(m -> {
              //            throw new IllegalStateException("이미 존재하는 회원입니다.");
              //        });
      
              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);
          }
      }
  • 회원 서비스 테스트
    package com.led.hellospring.service;
    
    import com.led.hellospring.domain.Member;
    import com.led.hellospring.repository.MemoryMemberRepository;
    import org.assertj.core.api.Assertions;
    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() {   // 테스트가 순서 의존적으로 작동하지 않도록, 각각 테스트의 끝마다 repository 변수를 비워주어야 함.
            memberRepository.clearStore();
        }
    
        @Test
        void join() {
            Member member = new Member();
            member.setName("spring");
    
            Long saveId = memberService.join(member);
    
            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);
            IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
    
            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    //        try {
    //            memberService.join(member2);
    //            fail();
    //        } catch (IllegalStateException e) {
    //            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    //        }
        }
    
        @Test
        void findMembers() {
        }
    
        @Test
        void findOne() {
        }
    }

스프링 빈과 의존관계

1. 회원 컨트롤러에 의존관계 추가

package com.led.hellospring.controller;

import com.led.hellospring.domain.Member;
import com.led.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;

    @Autowired // 스프링 컨테이너에 있는 memberService를 스프링이 연결해준다. <- memberService는 굳이 여러 객체를 생성해서 연결해줄 필요없기 때문에 하나만 생성하여 연결한다
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
}
  • 의존성 주입 (Dependency Injection) : (Autowired 어노테이션으로 표시된) 객체 의존관계를 스프링 컨테이너에서 찾아서 넣어준다. 스프링 컨테이너에 들어가는 객체는 (스프링 빈)어노테이션 등으로 정의해주어야 한다.
    • 필드 주입, setter 주입, 생성자 주입 3가지 방법이 있다. 컨테이너에 올리는 것은 초기단계이므로(굳이 나중에 컨테이너에 올리는 방식은 좋지않음) 생성자 주입을 권장.
  • 위의 회원 컨트롤러에서는, 여러개 있을 필요가 없는 memberService를 연결해준다. 다만 memberService가 현재 스프링 컨테이너에 올라와있지 않으므로, 올려줘야 한다.

  • 스프링 빈 : 스프링 컨테이너에 올라와있는 객체

<방법1> 컴포넌트 스캔 - 자동 의존관계 설정

  • 어노테이션 Component, Contoller, Service, Repository… : 자동 등록.
@Service
public class MemberService {
	private final MemberRepository memberRepository;
	@Autowired
	public MemberService(MemberRepository memberRepository) {
		this.memberRepository = memberRepository;
	}
}
@Repository
public class MemoryMemberRepository implements MemberRepository {}

<방법2> 직접 스프링 빈 등록 (위 방법1에서 했던 내용은 제거!. 추후 메모리 리포지토리를 변경할 것이므로 변경에 쉽게 스프링 빈을 직접 등록하는 방식으로 한다.)

    package com.led.hellospring;
    
    import com.led.hellospring.repository.MemberRepository;
    import com.led.hellospring.repository.MemoryMemberRepository;
    import com.led.hellospring.service.MemberService;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class SpringConfig {
    
        @Bean // 스프링 빈에 등록한다.
        public MemberService memberService() {
            return new MemberService(memberRepository());
        }
    
        @Bean
        public MemberRepository memberRepository() {
            return new MemoryMemberRepository();
        }
    }

회원 관리 예제 - 웹 MVC 개발

1. 회원 웹 기능 - 홈 화면 추가

  • 홈 컨트롤러 추가
    • 예전에 만든 정적 파일 (Index.html)이 있지만, 정적 파일보다 컨트롤러가 적재 우선 순위가 높기 때문에 “/”에서 해당 컨트롤러가 실행된다.

      package com.led.hellospring.controller;
      
      import org.springframework.stereotype.Controller;
      import org.springframework.web.bind.annotation.GetMapping;
      
      @Controller
      public class HomeController {
      
          @GetMapping("/")
          public String home() {
              return "home";
          }
      }
  • 회원 관리용 홈 (템플릿)
    // resource/templetes/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>

2. 회원 웹 기능 - 등록

  • 회원 등록 폼 컨트롤러
    public class MemberController {
        ...
        @GetMapping("/members/new")
        public String createForm() {
            return "members/createMemberForm";
        }
    }
  • 회원 등록 화면 + 폼 객체 (templete & object) : 타임리프가 받은 name을 필드로 가진 해당 폼 객체를 넘겨준다.
    // resources/templetes/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>
    package com.led.hellospring.controller;
    
    public class MemberForm {
        private String name;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }
  • 회원 등록 컨트롤러 : 앞서 등록 폼에서 버튼을 누르면 “/members/new”를 post한다. 이걸 처리해주어야함.
    public class MemberController {
        ...
    		@PostMapping("/members/new")
        public String create(MemberForm form) {
            Member member = new Member();
            member.setName(form.getName());
    
            memberService.join(member);
    
            return "redirect:/";
        }
    }

3. 회원 웹 기능 - 조회

  • 회원 컨트롤러에서 조회 기능
    public class MemberController {
        ...
    		@GetMapping("/members")
        public String list(Model model) {
            List<Member> members = memberService.findMembers();
            model.addAttribute("members", members);
            return "members/memberList";
        }
    }
  • 회원 리스트 html 템플릿
    • 타임리프 문법 : member객체를 반복해서 꺼내와 해당 id, name 속성을 출력.

      // resources/templetes/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}">
                      <td th:text="${member.id}"></td>
                      <td th:text="${member.name}"></td>
                  </tr>
                  </tbody>
              </table>
          </div>
      </div> <!-- /container -->
      </body>
      </html>

스프링 DB 접근 기술

1. H2 데이터베이스

  • H2 설치 : chmod 755 [h2.sh](http://h2.sh) 후, 쉘 실행.
    • JDBC URL은 처음에만 jdbc:h2:~/test로 연결. 그 이후로는 jdbc:h2:tcp://localhost/~/test 로 연결한다.

  • 테이블 생성
    drop table if exists member CASCADE;
    create table member
    {
    	id bigint generated by default as identity,
    	name varchar(255),
    	primary key (id)
    };
    • 테이블 관리를 위해 sql>ddl.sql 파일을 생성해 관리해도 좋다.

2. DB접근 - 순수 JDBC 사용하여

  1. build.gradle에 h2, jdbc 의존성 추가
dependencies {
	...
	implementation 'org.springframework.boot:spring-boot-starter-jdbc'
	runtimeOnly 'com.h2database:h2'
	...
}
  1. 데이터베이스 연결 설정 추가
// resources/application.properties
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
  • Jdbc 리포지토리 구현 (참고만)
    package com.led.hellospring.repository;
    
    import com.led.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 {
    
        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);
        }
    }
  • 스프링 설정 변경 (스프링 빈)
    • DataSource : 데이터베이스 커넥션을 획득할 때 사용하는 객체. 스프링 부트가 해당 datasource를 스프링 빈으로 만들어둔다.

      package com.led.hellospring;
      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.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import javax.sql.DataSource;
      @Configuration
      public class SpringConfig {
          private final DataSource dataSource;
          public SpringConfig(DataSource dataSource) {
              this.dataSource = dataSource;
      		}
          @Bean
          public MemberService memberService() {
              return new MemberService(memberRepository());
          }
          @Bean
          public MemberRepository memberRepository() {
      //      return new MemoryMemberRepository();
            return new JdbcMemberRepository(dataSource);
          }
      }

  • DI(Dependencies Injection)을 사용하면, 설정만 바꾸어 구현 클래스를 변경가능하다.

- 스프링 통합 테스트

package com.led.hellospring.service;

import com.led.hellospring.domain.Member;
import com.led.hellospring.repository.MemberRepository;
import com.led.hellospring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
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 //afterEach를 대신함. 트랜잭션을 실행하고, 테스트가 끝난뒤에 롤백해줌 -> 테스트 했던 데이터가 DB에 올라가지 않음.
class MemberServiceIntegrationTest {

    @Autowired MemberService memberService; // 테스이므로, 생성자 대신 필드기반으로 받기.
    @Autowired MemberRepository memberRepository;

    @Test
    void join() {
        Member member = new Member();
        member.setName("spring3");

        Long saveId = memberService.join(member);

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

    @Test
    public void 중복_회원_예외() {
        // given
        Member member1 = new Member();
        member1.setName("spring3");

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

        // when
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));

        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }
}

3. DB접근 - 스프링 JdbcTemplate 사용하여

  • JdbcTemplate 사용시 JDBC의 반복 코드가 대부분 제거된다. 다만 SQL은 직접 작성.
  • JdbcTemplate 회원 리포지토리 구현
    package com.led.hellospring.repository;
    
    import com.led.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 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 (rs, rowNum) -> {
                Member member = new Member();
                member.setId(rs.getLong("id"));
                member.setName(rs.getString("name"));
                return member;
            };
        }
    }
  • 스프링 설정 변경
    @Configuration
      public class SpringConfig {
          private final DataSource dataSource;
          public SpringConfig(DataSource dataSource) {
              this.dataSource = dataSource;
    }
          @Bean
          public MemberService memberService() {
              return new MemberService(memberRepository());
          }
          @Bean
          public MemberRepository memberRepository() {
    //        return new MemoryMemberRepository();
    //        return new JdbcMemberRepository(dataSource);
              return new JdbcTemplateMemberRepository(dataSource);
          }
    }

4. DB접근 - JPA

  • 기본적인 SQL을 JPA가 직접 만들어서 실행해준다.
    • 라이브러리 의존성 추가 (내부에 jdbc 관련 라이브러리를 포함하므로 주석처리)

      // build.gradle
      dependencies {
      	...
      	// implementation 'org.springframework.boot:spring-boot-starter-jdbc'
      	implementation 'org.springframework.boot:spring-boot-starter-data-jpa
      	...
      }
    • JPA 설정 추가

      // resources/application.properties
      ...
      spring.jpa.show-sql=true // JPA가 생성하는 SQL을 출력.
      spring.jpa.hibernate.ddl-auto=none // 테이블을 자동 생성하는 기능을 끔
  1. JPA 엔티티 매핑

    // com.led.hello-spring.domain/Member.java
    @Entity
    public class Member {
    
        @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String name;
    ...
  2. JPA 회원 리포지토리

    package hello.hellospring.repository;
    import hello.hellospring.domain.Member;
    import javax.persistence.EntityManager;
    import java.util.List;
    import java.util.Optional;
    public class JpaMemberRepository implements MemberRepository {
        private final EntityManager em;
        public JpaMemberRepository(EntityManager em) {
            this.em = em;
    		}
        public Member save(Member member) {
            em.persist(member);
            return member;
    		}
        public Optional<Member> findById(Long id) {
            Member member = em.find(Member.class, id);
            return Optional.ofNullable(member);
    		}
        public List<Member> findAll() {
            return em.createQuery("select m from Member m", Member.class)
                    .getResultList();
        }
        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();
    	} 
    }
  3. 서비스 계층에 트랜잭션 추가 : 스프링은 해당 클래스의 메소드를 실행할 때, 트랜잭션을 시작하고, 메소드가 정상 종료되면 트랜잭션을 커밋한다.

    @Transactional //org.springframework.transaction.annotation.Transactional
    public class MemberService {
  4. 스프링 설정 변경

    package com.led.hellospring;
    
    import com.led.hellospring.repository.*;
    import com.led.hellospring.service.MemberService;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import javax.persistence.EntityManager;
    import javax.sql.DataSource;
    
    @Configuration
    public class SpringConfig {
    		private final DataSource dataSource;
        private final EntityManager em;
    
        public SpringConfig(DataSource dataSource, EntityManager em) {
    		    this.dataSource = dataSource;
    		    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);
        }
    }

5. DB접근 - 스프링 데이터 JPA

  • findByName(), findByEmail() 처럼 메소드 이름만으로 조회 기능 제공
  • 페이징 기능 자동 제공
스프링 부트, JPA만 사용해도 개발 생산성 증가.
+ 스프링 데이터 JPA를 사용하면, 구현 클래스없이 인터페이스만으로 개발 완료.

참고: 실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용하고, 복잡한 동적 쿼리는 Querydsl 라이브러리
Querydsl을 사용하면 쿼리도 자바 코드로 안전하게 작성할 수 있고, 동적 쿼리도 편리하게 작성할 수 있다.
이 조합으로 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리를 사용하거나, 앞서 학습한 스프링 JdbcTemplate를 사용하면 된다.
  • 스프링 데이터 JPA 회원 리포지토리
    package com.led.hellospring.repository;
    
    import com.led.hellospring.domain.Member;
    import org.springframework.data.jpa.repository.JpaRepository;
    
    import java.util.Optional;
    
    public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository { // JpaRepository를 지니고 있으면, 자동으로 스프링 빈으로 등록된다.
    
        @Override
        Optional<Member> findByName(String name);
    
    }
  • 스프링 설정 변경
    package com.led.hellospring;
    
    import com.led.hellospring.repository.*;
    import com.led.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;
        }
    
        @Bean // 스프링 빈에 등록한다.
        public MemberService memberService() {
            return new MemberService(memberRepository);
        }
    }

AOP

  • AOP (Aspect Oriented Programming) : 시간 측정 로직(공통 관심 사항)을 핵심 관심 상황 각각에 원할 때 적용하여, 시간 측정을 깔끔하게 해보자.
    • AOP 적용시, 프록시라는 가짜를 먼저 세워두고, joinPoint 호출시 진짜를 호출하는 방식.

package com.led.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 TimeTraceApp {

    @Around("execution(* com.led.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");
        }
    }
}
START: execution(MemberService com.led.hellospring.SpringConfig.memberService())
END: execution(MemberService com.led.hellospring.SpringConfig.memberService()) 20ms
START: execution(String com.led.hellospring.controller.HomeController.home())
END: execution(String com.led.hellospring.controller.HomeController.home()) 6ms
START: execution(String com.led.hellospring.controller.MemberController.list(Model))
START: execution(List com.led.hellospring.service.MemberService.findMembers())
START: execution(List org.springframework.data.jpa.repository.JpaRepository.findAll())
END: execution(List org.springframework.data.jpa.repository.JpaRepository.findAll()) 112ms
END: execution(List com.led.hellospring.service.MemberService.findMembers()) 135ms
END: execution(String com.led.hellospring.controller.MemberController.list(Model)) 169ms

0개의 댓글