'공부/Spring _1-스프링 입문'에 해당되는 글 1건

반응형

스프링 입문 복습


(자바 문법을 모를땐, https://www.youtube.com/playlist?list=PLW2UjW795-f6xWA2_MUhEVgPauhGl3xIp 여기서 해당 부분을 참고하자!)

스프링 프로젝트 생성!

"Https//start.spring.io" => 스프링 관련 프로젝트를 생성해주는 site이다.

Maven/Gradle : 라이브러리를 가져오고 빌드하는 Life Cycle까지 관리해주는 툴이며 요즘은 Gradle을 주로 사용한다.

 

build.gradle

plugins {
	id 'org.springframework.boot' version '2.3.1.RELEASE'
	id 'io.spring.dependency-management' version '1.0.9.RELEASE'
	id 'java'
}
group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
    
repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'//thyleaf템플릿 엔진 사용 html틀 만들어줌
	implementation 'org.springframework.boot:spring-boot-starter-web'//해당 라이브러리 안에 톰캣 서버가 있음
	testImplementation('org.springframework.boot:spring-boot-starter-test') {
	exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }//Test를 위한 라이브러리들
}
	test {
	useJUnitPlatform()
}

 

View 환경설정

웰컴 페이지 생성 후 접속

 

localhost:8080/hello를 입력하면 내장톰캣서버가 받고 스프링컨테이너는 hello를 getmapping으로 입력받아

""@GetMapping("hello")" hello를 getmapping으로 받는 컨트롤러 hellocontroller를 실행하고

hellocontroller는 template/hello로 리턴하여 hello.html이 열리게 된다.

그리고 thymeleaf 엔진이 hello.html을 처리한다.

 

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

 

<controller/HelloController>

package com.example.hellospring.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

//웹 앱에서 첫번째 진입점이다.
@Controller
public class HelloController {

    //get메소드로 hello가 들어오면
    @GetMapping("hello")
    //spring이 모델을 만들어서 넣어준다.
    //Model은 HashMap 형태를 갖고 있으며, key, value값을 가지고 있습니다. 또한 addAttribute()와 같은 기능을 통해 모델에 원하는 속성과 그것에 대한 값을 주어 전달할 뷰에 데이터를 전달할 수 있습니다.
    public String hello(Model model){
        model.addAttribute("data", "hello!!");//속성, 값
        return "hello"; //template의 hello로
        //기본적으로 resources:template/+{viewname}+.html로 매핑된다.
        //따라서 resources/tempalate/hello.html로 이동한다
    }

}

 

 

<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>
    <!-- th는 thymeleaf 엔진을 뜻함-->
<p th:text="'안녕하세요. ' + ${data}" >안녕하세요. 손님</p>
</body>
</html>

data에 key값으로 hello!!를 넣어줬으므로 hello!!가 뜬다.

 

 

 

정적 컨텐츠

=> 파일을 그대로 뿌려준다. (파일 그대로 전달)

/static 폴더안의 파일을 찾는다.

 

"localhost:8080/hello-static.html"가 넘어오면 먼저 hello-static 관련 컨트롤러가 있는지 확인한다.

왜냐하면 컨트롤러가 우선순위를 가지기 때문이다.

스프링 컨테이너가 hello-static관련 컨트롤러가 없음을 확인하면 /static 폴더안을 살펴보고 있으면 그 파일을 반환한다.

 

<resources/static/hello-static.html>

<!DOCTYPE HTML>
<html>
<head>
    <title>static content</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
정적 컨텐츠 입니다.
</body>
</html>

 

 

MVC와 템플릿 엔진

Model, View, Controller로 분리하여 처리한다.

(예전에는 view에서 모든 로직을 다 처리하여 복잡했다.)

"localhost:8080/hello-mvc?name=spring"이 요청되면 @Getmapping("hello-mvc")가 받아서 뒤의 파라미터 name을 넘겨준다.

그리고 hello-template로 반환한다.

 

=> controller

package com.example.hellospring.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

//웹 앱에서 첫번째 진입점이다ㅣ.
@Controller
public class HelloController {

    @GetMapping("hello-mvc")
    //@RequestParam(value= "name", required = True ) ctrl+p 할 경우 필요한 파라미터 정보 볼수 있음
    //required가 기본이 트루이기 때문에 name값을 넘겨야됨
    //따라서 localhost:8080/hello-mvc?name=spring
    public String helloMvc(@RequestParam("name") String name, Model model){
        model.addAttribute("name", name);
        //모델이 name값을 넘겨줌
        return "hello-template";
    }


}

 

hello-template.html은 넘겨받은 name값을 표시한다.

<resources/templates/hello-template.html> => View

<html xmlns:th="http://www.thymeleaf.org">
<body>
<p th:text="'hello ' + ${name}">hello! empty</p>
    <!-- 템플릿 엔진이 model의 키 값에서 name인 것을 꺼낸다. 뒤의 hello! empty는 name값이 안넘어오면 표시된다. -->
</body>
</html>

 

 

API

주로 json포맷으로 data가 전달된다.

즉, data만 전달하는 경우이다.

"localhost:8080/hello-api?name=spring"

@ResponseBody를 사용하고 객체를 반환하면 객체가 json으로 변환된다.

(json은 key, value로 이루어진 데이터)

 

"localhost:8080/hello-api?name=spring"

요청시 @Controller가 있으므로 getmapping hello-api를 찾아서 실행한다.

@ResponseBody어노테이션이 있으므로, http의 body부에 그대로 값을 넘긴다.

(즉, viewResolver대신에 HttpMessageConverter가 동작한다.)

반환값이 단순 문자이므로 StringHttpMessageConverter가 기본문자를 처리한다.

원래는 @ResponseBody가 있으면 객체를 반환할 경우 MappingJackson2HttpMessageConverter에 의해서 객체를 자동으로 json타입으로 바꾸어서 반환한다. (key:value )와 같은 형태

-> 이것은 클라이언트의 HTTP Accept 헤더와 서버의 컨트롤러 반환 타입정보 두가지를 조합해서 선택한다.

(대부분은 json을 선택하여 ,json으로 반환한다.)

 

package com.example.hellospring.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

//웹 앱에서 첫번째 진입점이다ㅣ.
@Controller
public class HelloController {

    //문자를 넘기는 경우
     //get방식인데 body에 직접 전달함.
    //requestparam은 name으로 전달달
    @GetMapping("hello-string")
    @ResponseBody //http의 body부에 전달
    public String helloString(@RequestParam("name") String name) {
        return "hello " + name;//"hello spring" (StringConverter)
    }
    
    
    //객체를 넘기는 경우
    @GetMapping("hello-api")
    @ResponseBody//http의 body부에 전달 (viewResolver를 사용하지 않는다.), 객체반환시 json으로 반환된다.
    public Hello helloApi(@RequestParam("name") String name){
        Hello hello = new Hello();
        hello.setName(name);
        return hello;//Hello 객체를 넘기므로 json으로 바꿔짐 (json 컨버터가 바꿔줌(속성 : 값)으로)
        //jsonConverter
    }

    
   static class Hello{
        private String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }


}

 

 

 

 

회원 관리 예제


 

비지니스 요구 사항

  1. 데이터 = 회원id, 이름
  2. 기능 = 회원등록, 조회
  3. 데이터 저장소 = 아직 정해지지 않음

 

일반적인 웹 애플리케이션 계층 구조

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

서비스 : 핵심 비지니스 로직

리포지토리 : DB에 접근, 도메인 객체를 DB에 저장하고 관리

도메인 : 비지니스 도메인 객체 ex) 회원, 주문, 쿠폰 등등 주로 DB에 저장하고 관리됨

 

컨트롤러 -> 서비스 -> 리포지토리 -> DB

컨트롤러, 서비스, 리포지토리 -> 도메인

 

클래스 의존관계

멤버서비스 -> 멤버리포지토리(인터페이스) <- 메모리멤버리포지토리(구현체)

(저장소가 정해지지 않았으니까 일단 멤버리포지토리를 인터페이스로 구현해둠)

 

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

<회원 객체> = 회원 도메인

id와 name을 저장해야하므로 id와 name이 필요하다.

 

<멤버 도메인>

package com.example.hellospring.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

//1. 회원 id가 필요
//2. 회원 name이 필요
public class Member {

    //id, name 생성
    private Long id;
    private String name;


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }
}

 

 

<멤버리포지토리> -인터페이스 (구현은 데이터 저장소가 정해지면 구현)

package com.example.hellospring.repository;

import com.example.hellospring.domain.Member;

import javax.swing.text.html.Option;
import java.util.List;
import java.util.Optional;

/*
1. 저장
2. 찾기
2.1. id로 찾기
2.2. name으로 찾기
2.3. 전부 다 찾기
 */
public interface MemberRepository {
    //1. 저장(도메인은 member 클래스)
    Member save(Member member);

    //2.1 id로 찾기
    //optional은 null일경우 값이 없다고 알려준다.
    Optional<Member> findbyId(Long id);
    //2.2 name으로 찾기
    Optional<Member> findbyName(String name);
    //2.3 모두다 찾기
    //Member형의 List
    List<Member> findAll();

}

 

<메모리멤버리포지토리> - 멤버리포지토리를 일단 메모리에 저장, 따라서 멤버리포지토리 인터페이스 구현

package com.example.hellospring.repository;

import com.example.hellospring.domain.Member;
import org.springframework.stereotype.Repository;

import java.util.*;

//MemberRepository 구현
//리포지토리는 저장
//@Repository //리포지토리라고 알려줌 스프링에, 따라서 스프링이 가져옴 즉, 스프링 빈 등록
public class MemoryMemberRepository implements MemberRepository{

    //map을 이용하여 저장(키, 값)
    private static Map<Long, Member> store = new HashMap<>();
    // hashmap은 키와 값을 가지는 자료구조이다.
    //hasing을 사용하기 때문에 검색이 빠르다. 
    //A -> Hash(A) -> 값 :: A로 값이 매칭된다.
    private static long sequence = 0L;//시퀀스

    @Override
    public Member save(Member member) {
        //id값 설정
        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();
    }
}

 

 

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

자바는 JUnit이라는 프레임워크로 테스트를 실행할 수 있다.

따라서 반복실행, 여러테스트를 한꺼번에 할 수 있다.

"src/test/java" 하위 폴더에 생성한다.

 

package com.example.hellospring.repository;

import com.example.hellospring.domain.Member;
//import org.junit.jupiter.api.Assertions;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

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

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

//테스트만을 위해쓰니까 굳이 public으로 선언하지 않아도 됌
class MemoryMemberRepositoryTest {
    MemoryMemberRepository repository = new MemoryMemberRepository();


    //테스트가 끝날때 마다 동작
    //어떤 메소드가 동작이 끝나면 실행됨
    //한번에 여러테스트 진행시 db에 이전 테스트 데이터가 남으므로 메소드가 끝나면 데이터를 지워준다,.
    //테스트는 순서가 보장되지 않으므로 서로 독립적이게 설정해야함
    @AfterEach
    public void afterEach(){
        repository.clearStore();
    }


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

        repository.save(member);
        Member result = repository.findbyId(member.getId()).get();
        //단순히 출력해보아도됨 하지만 글자로 볼순 없음
        //Assertions.assertEquals(member, result);//기대, 비교값
        //member객체와 result 객체를 비교한다.
        //사용법만 익히자
        assertThat(member).isEqualTo(result); //멤버가 result와 같으냐
    }


    @Test
    public void findbyName(){
        //멤버1 생성후 리포지토리에 저장
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);

        //멤버2 생성후 리포지토리에 저장
        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);

        Member result = repository.findbyName("spring1").get();

        assertThat(result).isEqualTo(member1);
    }

    @Test
    public void findAll(){
        //멤버1 생성후 리포지토리에 저장
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);

        //멤버2 생성후 리포지토리에 저장
        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);

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

 

회원 서비스 개발

 

package com.example.hellospring.service;

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


public class MemberService {
    
    private final MemberRepository memberRepository = new MemberRepository();


    /**
     * 회원가입
     * 가입시 id값 반환
     * 먼저 중복 이름을 가지는 회원이 있는지 확인
     */
    public Long join(Member member){
        
        //만약 존재한다면 에러 던짐
        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();

    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findbyName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });//ctrl + T로 람다식 변환
    }

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

    /**
     * 한 회원만 조회
     */
    public Optional<Member> findOne(Long memberId){
        return memberRepository.findbyId(memberId);
    }
}

 

 

회원 서비스 테스트

package com.example.hellospring.service;

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


public class MemberService {
    
    //기존에는 회원 서비스가 메모리 회원 리포지토리를 직접 생성하게 했음
    //private final MemberRepository memberRepository = new MemberRepository();
    
    /*
    멤버 서비스에서 사용하는 멤버리포지토리랑
    멤버서비스테스트에서 사용하던 멤버리포지토리랑 다르다.
    멤버리포지토리의 store가 static이므로 같은 멤버리포지토리를 가르키지만 static이 아닌 경우에는 다르게된다.
    */
    private final MemberRepository memberRepository;
    
    //멤버리포지토리를 받아서 멤버 서비스에 넘겨준다.
	public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }


    /**
     * 회원가입
     * 가입시 id값 반환
     * 먼저 중복 이름을 가지는 회원이 있는지 확인
     */
    public Long join(Member member){
        
        //만약 존재한다면 에러 던짐
        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();

    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findbyName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });//ctrl + T로 람다식 변환
    }

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

    /**
     * 한 회원만 조회
     */
    public Optional<Member> findOne(Long memberId){
        return memberRepository.findbyId(memberId);
    }
}

 

멤버리포지토리를 받아서 멤버 서비스에 넘겨줫으므로 멤버서비스 테스트를 보면

package com.example.hellospring.service;

import com.example.hellospring.domain.Member;
import com.example.hellospring.repository.MemberRepository;
import com.example.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 java.util.List;
import java.util.Optional;

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 join() {

        //저장한게 리포지토리에 있는게 맞아?? => 멤버리포지토리 필요
        //given
        Member member = new Member();
        member.setName("hello");
        //만약 hello가 아니라 spring인 경우 밑 중복회원예외 메소드랑 값(db에 겹침)이 겹침

        //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);

        //2. assertThrows를 이용하는 방법
        //뒤 로직을 동작할때 앞의 예외가 터져야댐
        //assertThrows(IllegalStateException.class, () -> memberService.join(member2));

        //3. IllegalStateException 객체를 받아서 사용하는 방법법
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

        //1. try-catch를 사용하는 방법 ->복잡
        /*
        try{
            memberService.join(member2);
            fail();//혹시 catch로 안가면
        } catch (IllegalStateException e){
            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다."))
        }*/


    }
    @Test
    void findMember() {
    }

    @Test
    void findOne() {
    }
}

 

 

 

스프링 빈과 의존관계


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

회원 컨트롤러가 회원서비스와 회원 리포지토리를 사용할 수 있게 의존관계를 준비!

(회원 컨트롤러가 회원서비스와 회원리포지토르 두개를 통해서 데이터 조회를 할 수 있음.)

 

 

멤버컨트롤러에서 멤버서비스를 하나만 생성해두고 공용으로 사용한다.

이때 공용으로 사용하기 위해서 Autowired를 사용하여 컨트롤러 등록시 컨테이너가 객체를 생성해서 가지고 있게 한다.

package com.example.hellospring.controller;

import com.example.hellospring.domain.Member;
import com.example.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 {
    //하나만 생성해두고 공용으로 사용하면됨 굳이 new해서 새로 생성할 필요가 없다
    //private final MemberService memberService = new MemberService();
    //new해서 새로 생성해서 사용하면 멤버 컨트롤러 외의 다른 컨트롤러들(예를들어 주문)이 가져가서 쓸 수 있음.
    //하지만 멤버서비스를 가면 별기능이 없음. 따라서 하나만 생성해서 공용으로 사용하는 것이 나음
    //따라서 스프링 컨테이너에 등록을 하고 사용한다.   

    private final MemberService memberService;

    //컨트롤러 등록시 컨테이너가 객체를 생성(이때 생성자를 호출한다.)해서 들고 있게된다.
    //즉, 생성자에 Autowired가 선언되어 있으면, 멤버서비스를 스프링이 컨테이너에 있는 멤버서비스에 연결시켜준다.
    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }

    //회원등록 폼 이동
    @GetMapping("/members/new")
    public String createForm(){
        return "members/createMemberForm";
    }

    //회원 등록 값 입력후 회원 생성
    @PostMapping("/members/new")
    public String create(MemberForm form){
        Member member = new Member();
        member.setName(form.getName());

        memberService.join(member);

        return "redirect:/";
    }

    //회원 조회
    @GetMapping("/members")//members로 이동(home.html에서 회원목록 누르면 /members로 이동하게 되어 있음)
    public String list(Model model){
        List<Member> members = memberService.findMember();
        model.addAttribute("members", members);
        return "members/memberList";
    }
}

즉, 스프링컨테이너가 뜰때 MemnberController가 등록(스프링빈으로 등록)되는데 Autowired로 MemberSerivce를 연결했다.

하지만 MemberSerivce(@Controller 어노테이션이 없다!)는 순수 자바 파일이므로 스프링빈으로 등록되지 않았으므로 스프링 컨테이너 실행시 등록되어있지 않는다.

따라서 MemberSerivce에 @Service 어노테이션을 붙여주면 스프링이 올라올때 스프링 컨테이너에 멤버 서비스를 등록시켜준다.

 

package com.example.hellospring.service;

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


//Service어노테이션은 MemberService를 스프링 컨테이너에 등록시켜준다
//원래 멤버 서비스 클래스는 어떠한 어노테이션도 없었다.
//보통은 컨트롤러 어노테이션을 붙이면 스프링 컨테이너가 알아서 객체를 생성해서 갖고 있지만
//멤버 서비스는 그렇지 않았다.
@Service
public class MemberService {
    
    //기존에는 회원 서비스가 메모리 회원 리포지토리를 직접 생성하게 했음
    //private final MemberRepository memberRepository = new MemberRepository();
    
    /*
    멤버 서비스에서 사용하는 멤버리포지토리랑
    멤버서비스테스트에서 사용하던 멤버리포지토리랑 다르다.
    멤버리포지토리의 store가 static이므로 같은 멤버리포지토리를 가르키지만 static이 아닌 경우에는 다르게된다.
    */
    private final MemberRepository memberRepository;
    
    //멤버리포지토리를 받아서 멤버 서비스에 넘겨준다.
    @Autowired 
    //=>memberRepository가 필요하니까 연결, memberRepository는 @Repository로 스프링 빈 등록되어있음
	public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }


    /**
     * 회원가입
     * 가입시 id값 반환
     * 먼저 중복 이름을 가지는 회원이 있는지 확인
     */
    public Long join(Member member){
        
        //만약 존재한다면 에러 던짐
        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();

    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findbyName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });//ctrl + T로 람다식 변환
    }

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

    /**
     * 한 회원만 조회
     */
    public Optional<Member> findOne(Long memberId){
        return memberRepository.findbyId(memberId);
    }
}

 

 

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

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

    => 어노테이션을 사용한다. EX) @Service 어노테이션 안에는 @Component 어노테이션이 들어있다.

    컴포넌트 스캔 원리

    @Component 어노테이션이 있으면 스프링 빈으로 자동등록된다.

    객체를 생성해서 스프링에 등록한다. 그 후 Autowired가 각각 연결시켜준다.(각 객체들을)

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

    @Controller, @Service, @Repository 전부다 @Component를 포함하고 있다.

     

    따라서 MemoryMemberRepository 또한 어노테이션을 붙여준다.

    package com.example.hellospring.repository;
    
    import com.example.hellospring.domain.Member;
    import org.springframework.stereotype.Repository;
    
    import java.util.*;
    
    //MemberRepository 구현
    //리포지토리는 저장
    @Repository //리포지토리라고 알려줌 스프링에, 따라서 스프링이 가져옴 즉, 스프링 빈 등록
    public class MemoryMemberRepository implements MemberRepository{
    
        //map을 이용하여 저장(키, 값)
        private static Map<Long, Member> store = new HashMap<>();
        private static long sequence = 0L;//시퀀스
    
        @Override
        public Member save(Member member) {
            //id값 설정
            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();
        }
    }
    
    

     

    스프링컨테이너 안을 보면

    memberController --> memberService --> memberRepository 와 같이 연결되어있다.

    즉, memberController는 memberService, memberRepository가 필요하다.

    따라서 각각 @Controller, @Service, @Repository로 등록이 되고 사용시 @Autowired로 연결시켜준다.

     

    memberController --> memberService --> memberRepository

    이것들은 각각 스프링 빈인데, 스프링은 스프링 컨테이너에 스프링 빈을 등록할때 기본으로 싱글톤으로 등록한다.

    즉, 각 객체들을 하나만 등록해서 공유한다. => 같은 스프링빈이면 모두 같은 인스턴스이다.

    (memberController는 memberController 하나만, memberService는 memberService 하나만 등록과 같이)

    예를 들어 OrderService가 있어서 memberRepository에 접근시, 멤버 서비스나 오더 서비스는 동일한 멤버 리포지토리에 접근한다.

    따라서 메모리가 절약된다.

     

     

    만약 아무곳에나 @Component가 있어도 될까?(ex 다른곳에 아무 패키지 만들어서 demo라는 파일을 만들경우라던가)

    =>안된다.

    HelloSpring.java(main있음)를 실행시키면, Package hello.hellospring와 동일하거나 하위에 있는 것들은 스프링이 다 찾는다.

    즉, 다른 패키지에 있으면 스프링이 컴포넌트 스캔을 안하므로 등록이 안된다.

 

 

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

회원 서비스와 회원 리포지토리의 @Service, @Repository, @Autowired 어노테이션을 제거하고 진행한다.

 

hellospring/SpringConfig 생성

package com.example.hellospring;


import com.example.hellospring.repository.JdbcTemplateMemberRepository;
import com.example.hellospring.repository.JpaMemberRepository;
import com.example.hellospring.repository.MemberRepository;
import com.example.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;
import javax.sql.DataSource;

//자바 코드로 직접 스프링 빈 등록
@Configuration
public class SpringConfig {


    //직접 스프링 빈으로 등록한다고 알려줌으로써 DI(의존성 주입)를 해줌, 즉 연결시킴
    //스프링 빈을 등록할거임!! 알려줌줌 => 등록하렴!
    //자바 코드로 직접 멤버 서비스를 등록함/ @Service 어노테이션으로 등록하는게 아니라
    //직접 memberService 객체를 생성시켜서 등록함!
    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository()); //생성자에서 memberRepository를 넣어줘야함
        //밑의 memberRepository를 엮어야댐
    }

    //생성자에서 memberRepository를 넣어줘야하므로 등록시켜줌 리포지토리도
    @Bean
    public MemberRepository memberRepository(){
        return new MemoryMemberRepository(); 


    /**
     * 실행시 멤버 서비스와 멤버리포지토리를 스프링 빈에 등록하고
     * 스프링 빈에 등록된 멤버리포지토리를 멤버서비스에 넣어줌
     * 따라서 memberService -> memberRepository 연결됨
     * @Controller는 스프링이 관리하는거이기때문에 어쩔 수 없다.
     * 컴포넌트 스캔으로 올라가고 컴포넌트 스캔이므로 Autowired로 연결되어야한다.
     * 장단점이 있다.
     *
     */


}

 

DI에는

  • 필드주입 = @Autowired

    @Autowired를 통한 DI는 helloController, memberService 등과 같이 스프링이 관리하는 객체에서만 동작을한다!

    스프링빈으로 등록하지 않고 직접 생성한 객체에서는 동작하지 않는다.

    왜냐하면 스프링 컨테이너가 들고 있지 않으니까!

 

  • 생성자 주입 = 생성자를 통해서 주입

  • setter 주입 = setter 설정으로 생성하고 @Autowired (말 그대로 setter로 주입됨.)

    (누군가 멤버 컨트롤러를 호출 했을때 public으로 열려 있어야함.

    => public하게 노출되어버린다.(아무나 변경가능해진다.) 중간에 변경되면 문제가 생길 수 도 있다.)

 

** 실무에서는 주로 정형화된 컨트롤러, 서비스, 리포지토리 같은 코드는 컴포넌트 스캔방식을 사용한다. 정형화 되지 않거나 상황에 따라 구현 클래스를 변경해야하마녀 설정을 통해 스프링 빈으로 등록한다.

{정형화(보통 일반적으로 자주 쓰는 틀로 만든(저장, 조회 등 일반적으로 자주 쓰는))}

 

==> 향후 메모리 리포지토리에서 다른 리포지토리로 변경할 예정이므로 컴포넌트 스캔방식이 아닌 자바코드로 스프링 빈을 설정했다.

 

 

 

회원 관리 예제 - 웹 MVC 개발


회원 웹 기능 - 홈 화면 추가

 

HomeController 추가

package com.example.hellospring.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {

    //컨트롤러가 정적파일보다 우선순위가 높음
    //도메인 "/"
    @GetMapping("/")
    public String home(){
        return "home"; //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>

 

 

회원 웹기능 - 등록

회원 등록 폼 컨트롤러

package com.example.hellospring.controller;

import com.example.hellospring.domain.Member;
import com.example.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 {
    //하나만 생성해두고 공용으로 사용하면됨 굳이 new해서 새로 생성할 필요가 없다
    //private final MemberService memberService = new MemberService();
    //따라서 스프링 컨테이너에 등록을 하고 사용한다.

    private final MemberService memberService;

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

    //member/new 요청시 member/createMemberForm 반환
    //회원등록 폼 이동
    @GetMapping("/members/new")
    public String createForm(){
        return "members/createMemberForm";
    }



}

 

회원 등록 폼 HTML

(resources/templates/members/createMemberForm)

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
    <!-- 등록을 누르면 Post방식으로 넘어온다. (데이터 전달) "/members/new"에 전달해줌-->
    <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.example.hellospring.controller;

public class MemberForm {
    private String name;//name을 입력받음

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

 

그러면 이제 실제 회원을 등록하는 기능을 MemberController에 추가해준다.

package com.example.hellospring.controller;

import com.example.hellospring.domain.Member;
import com.example.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 {
    //하나만 생성해두고 공용으로 사용하면됨 굳이 new해서 새로 생성할 필요가 없다
    //private final MemberService memberService = new MemberService();
    //따라서 스프링 컨테이너에 등록을 하고 사용한다.

    private final MemberService memberService;

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

    //회원등록 폼 이동
    @GetMapping("/members/new")
    public String createForm(){
        return "members/createMemberForm";
    }

    /*==========================================================*/
    //회원 등록 값 입력후 회원 생성
    //post로 /members/new를 가져옴
    @PostMapping("/members/new")
    public String create(MemberForm form){ //위에서 만든 MemberForm으로 데이터 받음
        Member member = new Member();
        member.setName(form.getName());

        memberService.join(member);

        return "redirect:/";//홈화면으로 보냄
        
    }
    /*==========================================================*/

}

 

 

회원 웹 기능 - 조회

회원 컨트롤러에 조회기능 생성

package com.example.hellospring.controller;

import com.example.hellospring.domain.Member;
import com.example.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 {
    //하나만 생성해두고 공용으로 사용하면됨 굳이 new해서 새로 생성할 필요가 없다
    //private final MemberService memberService = new MemberService();
    //따라서 스프링 컨테이너에 등록을 하고 사용한다.

    private final MemberService memberService;

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

    //회원등록 폼 이동
    @GetMapping("/members/new")
    public String createForm(){
        return "members/createMemberForm";
    }

    //회원 등록 값 입력후 회원 생성
    @PostMapping("/members/new")
    public String create(MemberForm form){
        Member member = new Member();
        member.setName(form.getName());

        memberService.join(member);

        return "redirect:/";
    }

    /*===============================================*/
    //회원 조회
    @GetMapping("/members")//members로 이동(home.html에서 회원목록 누르면 /members로 이동하게 되어 있음)
    public String list(Model model){
        List<Member> members = memberService.findMember();
        model.addAttribute("members", members);
        return "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>
                <!-- members를 읽어들이고 id와 name가져옴(thymeleaf버전의 for each같은 문법임)
				즉 회원들을 조회를 하는 것임-->
            <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 접근 기술


H2 데이터베이스를 사용하여 스프링에서 접근해본다.

h2데이터베이스는 개발이나 테스트 용도로 가볍고 편리한 DB, 웹화면을 제공해준다.

 

drop table if exists member CASACDE;
create table member{
	id bigint getnerated by default as identity, 
--값을 입력하지 않으면 DB가 알아서 채운다는 뜻이다.(identity 방식)
	name varchar(255),
	primary key(id)
}

 

순수 Jdbc

고대의 기술이라 불린다...

대충 이렇구나만 하고 넘어가자..

 

gradle.build에 관련 라이브러리 추가

implementation 'org.springframework.boot:spring-boot-starter-jdbc'//자바-db 연동시 필요
runtimeOnly 'com.h2database:h2'//db가 제공하는 클라이언트

 

스프링 부트 데이터베이스 연결 설정 추가

resources/application.properties

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

이렇게 세팅하면 스프링 부트가 DataSource를 만들어 놓는다.

즉, 데이터베이스 접속 정보를 만들어 놓는다.

 

Jdbc 회원 리포지토리

package com.example.hellospring.repository;


import com.example.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;
    }
    //저장을 위해서 insert 쿼리가 필요하다 따라서 직접 작성
    @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(); //DB 연결 만듬
            pstmt = conn.prepareStatement(sql,
                    Statement.RETURN_GENERATED_KEYS);//sql 입력
            pstmt.setString(1, member.getName()); // sql의 ?에 매칭됨
            pstmt.executeUpdate(); //DB에 쿼리 날림
            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);
    }
}

 

 

스프링 설정 변경

package com.example.hellospring;


import com.example.hellospring.repository.JdbcTemplateMemberRepository;
import com.example.hellospring.repository.JpaMemberRepository;
import com.example.hellospring.repository.MemberRepository;
import com.example.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;
import javax.sql.DataSource;

//자바 코드로 직접 스프링 빈 등록
@Configuration
public class SpringConfig {

    private final DataSource dataSource;
    private final EntityManager em;


    @Autowired //생성자가 하나라면 생략가능
    public SpringConfig(DataSource dataSource, EntityManager em) {
        this.dataSource = dataSource;
        this.em = em;
    }

    //스프링 빈을 등록할거임!! 알려줌줌 => 등록하렴!
    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository()); //생성자에서 memberRepository를 넣어줘야함
        //밑의 memberRepository를 엮어야댐
    }

    //생성자에서 memberRepository를 넣어줘야하므로 등록시켜줌 리포지토리도
    @Bean
    public MemberRepository memberRepository(){
        //return new MemoryMemberRepository(); //MemberRepository는 인터페이스 이니까
        return new JdbcMemberRepository(dataSource); //이 코드추가로 메모리멤버리포지토리에서 jdbc리포지토리로 바뀌엇다.
        //다른 코드를 변경하지 않고 저장소를 단 한줄로 바꾸었따.!!
 

    /**
     * 실행시 멤버 서비스와 멤버리포지토리를 스프링 빈에 등록하고
     * 스프링 빈에 등록된 멤버리포지토리를 멤버서비스에 넣어줌
     * 따라서 memberService -> memberRepository 연결됨
     * @Controller는 스프링이 관리하는거이기때문에 어쩔 수 없다.
     * 컴포넌트 스캔으로 올라가고 컴포넌트 스캔이므로 Autowired로 연결되어야한다.
     * 장단점이 있다.
     *
     */


}

DataSource는 데이터베이스 커넥션을 획득할 때 사용하는 객체다. 스프링 부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어둔다. 그래서 DI를 받을 수 있다.

스프링 부트가 DataSource를 생성하고 스프링 빈 등록하니까 주입받아서 쓸수 있음

 

OCP (Open-Closed 원칙)

개방-폐쇄 원칙.

확장에는 열려있고 수정 변경에는 닫혀있다.

스프링의 DI를 사용하면 기존코드를 손대지않고 설정만으로 구현클래스를 변경할 수 있다.

위의 예시서첨 Memory멤버리포지토리에서 코드만 변경하여 jdbc멤버 리포지토리로 변경한것처럼.

 

스프링 통합 테스트

스프링 컨테이너와 DB까지 연결한 통합 테스트!

(이전 테스트코드들은 그냥 자바파일들이엿지만, 지금은 데이터가 DB에 저장되고 DB연결 정보도 스프링이 가지고 있따.)

package com.example.hellospring.service;

import com.example.hellospring.domain.Member;
import com.example.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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

@SpringBootTest//테스트시 스프링부트 가져옴 (스프링 컨테이너와 테스트를 함께 실행한다.)
@Transactional//데스트시 DB 트랜잭션 롤백시킴(테스트가 끝나면 커밋 안함), 따라ㅓㅅ 다음 테스트에 영향안줌
public class MemberServiceIntegrationTest {
    
    //Test 케이스 이므로 그냥 필드 주입시킴
    //new 로 생성하지 않고 스프링에게 달라고 요청함
    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;

    @Test
    public void 회원가입() throws Exception {
        //Given
        Member member = new Member();
        member.setName("hello");
        //When
        Long saveId = memberService.join(member);
        //Then
        Member findMember = memberRepository.findbyId(saveId).get();
        assertEquals(member.getName(), findMember.getName());
    }
    @Test
    public void 중복_회원_예외() throws Exception {//thorws Exception => 이 메소드가 예외를 던질 수 도 있음
        //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("이미 존재하는 회원입니다.");
    }
}


 

 

스프링 JdbcTemplate

순수 jdbc에서 봣던 반복 코드들을 대부분 제거해준다.

하지만 SQL은 직접 작성해야한다.

(Jdbc => DB와 스프링 중간에서 번역해서 아무 DBMS에 다 맞게 번역해줌)

package com.example.hellospring.repository;

import com.example.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.print.attribute.HashPrintJobAttributeSet;
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 {

    //JdbcTemplate 사용!!
    private final JdbcTemplate jdbcTemplate;//인젝션 받을 수 있는건 아님

    //JdbcTemplate 사용시 DataSource가 필요하다.
    @Autowired //생성자가 하나인 경우에 생략가능하다.
    public JdbcTemplateMemberRepository(DataSource dataSource) {
        jdbcTemplate = new JdbcTemplate(dataSource);//datasource = db접근 정보 넘겨줌
    }

    //save하는 경우는 DB에 Insert하는 경우이다. 따라서 명시적으로 insert하기 위해 SimpleInsert사용
    @Override
    public Member save(Member member) {
        /*
        SimpleJdbcInsert은 알아서 insert문을 생성해준다.
        테이블명과 키 컬럼을 명시해주면 알아서 insert해준다.
        또한 이전 jdbc와 다르게 ?에 값을 매칭시키는게 아니라 명시적으로 이름을 지정하여 의존성을 높여준다.
        만약 ?, ?로 두개의 값을 매칭할때 두 값이 순서가 바뀌면 문제가 되버리니 명시적으로 지정한다.
        */
        SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
        jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");
        //테이블명은 member이고 키 컬럼은 id이다.

       
        Map<String, Object> parameters = new HashMap<>();
        parameters.put("name", member.getName()); //parameters는 Map타입이므로 각 값들 put해줌

        //key를 받고 넘겨줌, key는 sql 결과
        //이구문은 다시한번 봐야할듯!!!!
        //파라미터를 넘겨서 실행하고 생성된 키 값을 받아옴
        Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
        member.setId(key.longValue());//member에 setID해서 넣어줌 id값
        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();//stream으로 변환해서 하나라도 있으면 반환
    }

    @Override
    public Optional<Member> findbyName(String name) {
        //Member객체들의 List이므로 List<Member>라고 사용
        List<Member> result = jdbcTemplate.query("select * from member where name=?", memberRowMapper(), name);
        return result.stream().findAny();
    }

    @Override
    public List<Member> findAll() {
        //memberRowMapper는 member객체를 반환하므로 그대로 씀
        //해당 쿼리 결과(리스트로 반환됨)을 memberRowMapper()가 매핑해준다. 그리고 그걸 반환함
        // sql, 반환타입, 인자 (현재 sql문에는 인자가 필요없으므로 없다.)
        return jdbcTemplate.query(("select * from member"), memberRowMapper());
    }

    //ResultSet rs를 받아서 rowNum만큼 반복해서 member객체에 id와 name을 설정해준다.
    private RowMapper<Member> memberRowMapper(){
            return (rs, rowNum) -> {
                Member member = new Member();
                //rs로 결과를 받아서 member에 매핑해서 member를 반환한다.
                member.setId(rs.getLong("id"));
                member.setName(rs.getString("name"));
                return member;

            };
        
     /*
     템플릿으로부터 ResultSet을 받고 필요한 정보를 추출해서 리턴한다.
     ResultSet의 row하나만 매핑하기 위해 사용한다.
     */
            /*
            Alt+Enter로 람다식으로 바꿀 수 있음
            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;

                }
            };
             */
    }
}

 

RowMapper

기존에는

ResultSet rs = stat.executeQuery("select ~~~ ")
while(rs.next()){//다음 row로
    user = new User();
    ~~setId
    ~~setName
        ...
}

위와 같은 방식이였다.

즉, ResultSet으로 값을 받고 그 다음 User 객체에 담아서 반환하는 방식이였다.

(

ResultSet(java.sql.ResultSet)은 즉 결과임 쿼리문에 대한 결과임.

Select문 결과 조회할 수 있는 방법을 정의한 인터페이스이다.

즉, 결과 집합이다.

next(), getString(), getInt() ...등을 사용해서 값을 가져올수 잇다.

)

JdbcTemplate는 이것을 줄여서 쓴다.

//ResultSet rs를 받아서 rowNum만큼 반복해서 member객체에 id와 name을 설정해준다.
    private RowMapper<Member> memberRowMapper(){
            return (rs, rowNum) -> {
                Member member = new Member();
                member.setId(rs.getLong("id"));
                member.setName(rs.getString("name"));
                return member;

            };

rs( = ResultSet)이 값을 가져오고

rowNum만큼 반복해서

객체에 저장해서 반환한다.

 

스프링 설정에서 기존 jdbc에서 jdbcTemplate으로 리포지토리를 바꾼다.

package com.example.hellospring;


import com.example.hellospring.repository.JdbcTemplateMemberRepository;
import com.example.hellospring.repository.JpaMemberRepository;
import com.example.hellospring.repository.MemberRepository;
import com.example.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;
import javax.sql.DataSource;

//자바 코드로 직접 스프링 빈 등록
@Configuration
public class SpringConfig {

    private final DataSource dataSource;
    private final EntityManager em;


    @Autowired
    public SpringConfig(DataSource dataSource, EntityManager em) {
        this.dataSource = dataSource;
        this.em = em;
    }

    //스프링 빈을 등록할거임!! 알려줌줌 => 등록하렴!
    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository()); //생성자에서 memberRepository를 넣어줘야함
        //밑의 memberRepository를 엮어야댐
    }

    //생성자에서 memberRepository를 넣어줘야하므로 등록시켜줌 리포지토리도
    @Bean
    public MemberRepository memberRepository(){
        //return new MemoryMemberRepository(); //MemberRepository는 인터페이스 이니까
        //return new JdbcMemberRepository(dataSource); //이 코드추가로 메모리멤버리포지토리에서 jdbc리포지토리로 바뀌엇다.
        return new JdbcTemplateMemberRepository(dataSource);//jdbc 템플릿으로 바꿈 레포지토리를

    }

    /**
     * 실행시 멤버 서비스와 멤버리포지토리를 스프링 빈에 등록하고
     * 스프링 빈에 등록된 멤버리포지토리를 멤버서비스에 넣어줌
     * 따라서 memberService -> memberRepository 연결됨
     * @Controller는 스프링이 관리하는거이기때문에 어쩔 수 없다.
     * 컴포넌트 스캔으로 올라가고 컴포넌트 스캔이므로 Autowired로 연결되어야한다.
     * 장단점이 있다.
     *
     */


}

 

 

JPA


jpa는 개 편한방식이다...

기존 반복코드도 줄여주고, 기본적인 sql문도 자기가 직접 다 만들어준다.

따라서 개발 생산성이 크게 높아진다.

 

gradle에 관련 라이브러리 추가

plugins {
	id 'org.springframework.boot' version '2.5.10'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	//implementation 'org.springframework.boot:spring-boot-starter-jdbc'//자바-db 연동시 필요
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'//jpa, jdbc다 포함함
	runtimeOnly 'com.h2database:h2'//db가 제공하는 클라이언트
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

 

스프링 부트에 JPA 설정 추가

resources/application.properties

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

spring.jpa.show-sql=ture//jpa가 날리는 쿼리를 볼수잇게 해줌
spring.jpa.hibernate.ddl-auto=none
    //JPA를 사용하면 member객체를 보고 자기가 table을 자동으로 만든다.
    //하지만 위에서 이미 테이블을 다 만들었으므로 이 옵션은 끔
    //자동 생성을 원하면 create로 설정해두면됨

 

JPA는 인터페이스(자바 표준 인터페이스, 구현은 여러업체에서한다.)이다. 인터페이스만 제공해준다.

구현체로 hibernate ...등이 있다. (대부분 hibernate만 사용한다.)

JPA는 객체와 ORM(object, Relational(관계형DB), Mapping(어노테이션으로))

 

JPA 엔티티 매핑

package com.example.hellospring.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

//1. 회원 id가 필요
//2. 회원 name이 필요
@Entity//jpa가 관리하는 엔티티라고 알림
public class Member {

    //id -> p.k라고 매핑시킴, 기본키 지정시 설정해야됨 db에서 자동으로 id값을 생성해주므로 identiy라고 적음
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    //id, name정보로 JPA는 sql문을 자동으로 작성해준다.
    //ex) @Column (name = "username") -> 만약 DB컬럼명이 username인경우, 매핑시킴 (DB에 있는 컬럼명은 username이라고 매핑됨))

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }
}

 

JPA 회원 리포지토리

package com.example.hellospring.repository;

import com.example.hellospring.domain.Member;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import javax.swing.text.html.parser.Entity;
import java.util.List;
import java.util.Optional;


public class JpaMemberRepository implements MemberRepository {

    /*
    gradle에서 "~~-data-jpa"라이브러리를 받으면
    스프링부트가 자동으로 EntityManager(DB랑 연결)를 생성해준다.
     */
    private final EntityManager em;

    //injection 받음
    public JpaMemberRepository(EntityManager em) {
        this.em = em;
    }

    @Override
    public Member save(Member member) {
        //persist => 영구 저장하다.
        em.persist(member);//쿼리는 알아서 다 작성해줌
        return member;
    }

    @Override
    public Optional<Member> findbyId(Long id) {
        //find(타입, 식별값=p.k)
        Member member = em.find(Member.class, id);
        return Optional.ofNullable(member);
    }

    @Override
    public Optional<Member> findbyName(String name) {
        //JPQL이다. 객체대상으로 쿼리를 날리면 sql로 번역이된다. P.K가 아닌 경우에 사용된다.
        //JPQL, 반환Type
        List<Member> result = em.createQuery("select m from Member m where m.name= :name", Member.class)
                .setParameter("name", name) //name컬럼에서 name값 설정
                .getResultList();
        return result.stream().findAny();//해당 name을 찾으면 반환함
    }

    @Override
    public List<Member> findAll() {
        //기본키가 아니므로 JPQL문을 사용하여 가져옴
        //m은 member Entity 자체를 select(이미 다 매핑되어있음)
        //결과를 list로 가져옴
        return em.createQuery("select m from Member m", Member.class).getResultList();
    }
}

 

그리고 서비스 계층에 트랜젝션 어노테이션을 추가해야된다.

왜냐면 DB에 접근하기때문에.

스프링은 해당 클래스의 메서드를 실행할때 트랜잭션을 시작하고 메서드가 종료되면 트랜잭션을 커밋함.

만약 런타임 예외가 발생하면 롤백한다.

JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행해야 한다.

package com.example.hellospring.service;

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

//Service어노테이션은 MemberService를 스프링 컨테이너에 등록시켜준다
//원래 멤버 서비스 클래스는 어떠한 어노테이션도 없었다.
//보통은 컨트롤러 어노테이션을 붙이면 스프링 컨테이너가 알아서 객체를 생성해서 갖고 있지만
//멤버 서비스는 그렇지 않았다.
//@Service
@Transactional //data를 저장하거나 변경하므로 필요하다.
//해당 클래스의 메서드를 실행할때 트랜잭션을 시작하고 종료되면 트랜잭션을 커밋함, 런타임 예외시는 롤백
public class MemberService {
    
    //private final MemberRepository memberRepository = new MemberRepository();
    private final MemberRepository memberRepository;

    //alt+ insert => constructor 생성
    //외부에서 멤버리포지토리를 넣어주게 만듬 //테스트환경에서 필요해짐
    //생성자에서 멤버리포지토리가 필요하므로 스프링이 가져와서 넣어줌(MemberRepository는 MemoryMemberRepository가 구현함)
    //따라서 MemoryMemberRepository를 연결시켜준다.(서비스에 주입시켜줌)
    //@Autowired //스프링 빈에 등록
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    /*
    setter 주입법
    private memberRepository memberRepository; //주의! -> final 선언이 아님

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

    이러면 다른곳에서 memberService.setMemberService()로 변경가능해진다.
    따라서 별로 잘 안씀

    의존관계가 실행중에 동적으로 변하는 경우는 잘 없다.!
     */


    /**
     * 회원가입
     * 가입시 id값 반환
     * 먼저 중복 이름을 가지는 회원이 있는지 확인
     */
    public Long join(Member member){
        
        //만약 존재한다면 에러 던짐
        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();

    }

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

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

    /**
     * 한 회원만 조회
     */
    public Optional<Member> findOne(Long memberId){
        return memberRepository.findbyId(memberId);
    }
}

 

JPA를 사용하도록 스프링 설정을 변경해준다.

package com.example.hellospring;


import com.example.hellospring.repository.JdbcTemplateMemberRepository;
import com.example.hellospring.repository.JpaMemberRepository;
import com.example.hellospring.repository.MemberRepository;
import com.example.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;
import javax.sql.DataSource;

//자바 코드로 직접 스프링 빈 등록
@Configuration
public class SpringConfig {

    private final DataSource dataSource;
    private final EntityManager em;


    @Autowired
    public SpringConfig(DataSource dataSource, EntityManager em) {
        this.dataSource = dataSource;
        this.em = em;
    }

    //스프링 빈을 등록할거임!! 알려줌줌 => 등록하렴!
    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository()); //생성자에서 memberRepository를 넣어줘야함
        //밑의 memberRepository를 엮어야댐
    }

    //생성자에서 memberRepository를 넣어줘야하므로 등록시켜줌 리포지토리도
    @Bean
    public MemberRepository memberRepository(){
        //return new MemoryMemberRepository(); //MemberRepository는 인터페이스 이니까
        //return new JdbcMemberRepository(dataSource); //이 코드추가로 메모리멤버리포지토리에서 jdbc리포지토리로 바뀌엇다.
        //return new JdbcTemplateMemberRepository(dataSource);//jdbc 템플릿으로 바꿈 레포지토리를
        return new JpaMemberRepository(em);//jpa는 EntityManager가 필요하다.
    }

    /**
     * 실행시 멤버 서비스와 멤버리포지토리를 스프링 빈에 등록하고
     * 스프링 빈에 등록된 멤버리포지토리를 멤버서비스에 넣어줌
     * 따라서 memberService -> memberRepository 연결됨
     * @Controller는 스프링이 관리하는거이기때문에 어쩔 수 없다.
     * 컴포넌트 스캔으로 올라가고 컴포넌트 스캔이므로 Autowired로 연결되어야한다.
     * 장단점이 있다.
     *
     */


}

 

 

 

스프링 데이터 JPA


스프링 부트와 jpa를 사용하면 편하지만, 여기에 스츠링 데이터 jpa를 사용하면 더욱 편해진다.

예를 들어 리포지토리에 구현 클래스 없이 인터페이스만으로도 개발을 완료 할 수 있다.

즉, 단순 반복도 줄이고 기본 CRUD기능도 스프링 데이터 jpa가 모두 제공한다.

 

crud => 데이터 생성(Create), 검색(Read), 갱신(Update), 삭제(Delete)

 

스프링 데이터 JPA 회원 리포지토리

package com.example.hellospring.repository;

import com.example.hellospring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;//스프링 데이터 jpa가 제공함(인터페이스에 대한 구현체를 자동으로 만든다.)

import java.util.Optional;

//인터페이스 다중상속
//JpaRepository 와 MemberRepository 인터페이스를 가져옴
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
    Optional<Member> findByName(String name);// 구현 완료!!! 나머진 데이터 JPA CRUD 다 만들어준다.

}

 

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

package com.example.hellospring;


import com.example.hellospring.repository.MemberRepository;
import com.example.hellospring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.swing.*;

//자바 코드로 직접 스프링 빈 등록
@Configuration
public class SpringConfig {
    
    private final MemberRepository memberRepository;

    public SpringConfig(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    //스프링 컨테이너가 멤버리포지토리를 찾는다.
    //하지만 등록한게 없다. 킹치만.. => SpringDataJpaMemberRepository 가 있음
    //스프링 데이터 JPA가 자동으로 SpringDataJpaMemberRepository를 스프링 빈으로 자동등록해줌
    
    //memberRepository를 인젝션 받아서 등록해준다.
    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository);
    }
    //스프링빈 등록해줌
    
    
/*
    private final DataSource dataSource;
    private final EntityManager em;

    @Autowired
    public SpringConfig(DataSource dataSource, EntityManager em) {
        this.dataSource = dataSource;
        this.em = em;
    }

    //스프링 빈을 등록할거임!! 알려줌줌 => 등록하렴!
    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository()); //생성자에서 memberRepository를 넣어줘야함
        //밑의 memberRepository를 엮어야댐
    }

    //생성자에서 memberRepository를 넣어줘야하므로 등록시켜줌 리포지토리도
    @Bean
    public MemberRepository memberRepository(){
        //return new MemoryMemberRepository(); //MemberRepository는 인터페이스 이니까
        //return new JdbcMemberRepository(dataSource); //이 코드추가로 메모리멤버리포지토리에서 jdbc리포지토리로 바뀌엇다.
        //return new JdbcTemplateMemberRepository(dataSource);//jdbc 템플릿으로 바꿈 레포지토리를
        return new JpaMemberRepository(em);//jpa는 EntityManager가 필요하다.
    }

    /**
     * 실행시 멤버 서비스와 멤버리포지토리를 스프링 빈에 등록하고
     * 스프링 빈에 등록된 멤버리포지토리를 멤버서비스에 넣어줌
     * 따라서 memberService -> memberRepository 연결됨
     * @Controller는 스프링이 관리하는거이기때문에 어쩔 수 없다.
     * 컴포넌트 스캔으로 올라가고 컴포넌트 스캔이므로 Autowired로 연결되어야한다.
     * 장단점이 있다.
     *
     */

*/
}

 

 

스프링 데이터( Repository(interface) <- CrudRepository(interface) <- PagingAndSortingRepository(interface )) <- 스프링 데이터 JPA(JpaRepository(interface)) 와 같은 관계를 가진다.

각 리포지토리들에 기본적인 CRUD 메소드들과 자주 사용하는 기본 메소드들이 있다.

따라서 자동으로 다 생성해준다.

그러므로 가져다쓰기만 하면됌!

findByName()이나 findByEmail()처럼 메서드 이름만으로 조회 기능을 제공해준다.

 

package com.example.hellospring.repository;

import com.example.hellospring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;//스프링 데이터 jpa가 제공함(인터페이스에 대한 구현체를 자동으로 만든다.)

import java.util.Optional;

//인터페이스 다중상속
//JpaRepository 와 MemberRepository 인터페이스를 가져옴
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
    //JPA는 select m from Member m where m.name=?로 JPQL을 짠다.
    //메소드 이름이 findBy"Name"이니까!!
    //따라서 name이 아니라 username이라던가 nameAndid라고 되어잇는경우는
    //By뒤에만 바꾸어 주면 나머지는 스프링 데이터 JPA가 알아서 다 짜준다.
    Optional<Member> findByName(String name);

}

 

복잡한 동적쿼리는 Querydsl이라는 라이브러리를 사용하면 된다.

이 조합으로 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리를 사용하거나 스프링 JdbcTemplate를 사용하면된다.

 

728x90
반응형
블로그 이미지

아상관없어

,