Hello It's good to be back ^_^

[ECC-백엔드 5팀] 9주차 스터디 본문

Study/Spring Boot

[ECC-백엔드 5팀] 9주차 스터디

HongyeonLee 2026. 5. 16. 23:57
강의: 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 

범위: 섹션 6-7

실습: https://github.com/HongYeonLee/ECC-Backend-Study

 

GitHub - HongYeonLee/ECC-Backend-Study: ECC 51기 SS 백엔드 5팀 스터디 내용을 기록하는 레포지토리입니다

ECC 51기 SS 백엔드 5팀 스터디 내용을 기록하는 레포지토리입니다. Contribute to HongYeonLee/ECC-Backend-Study development by creating an account on GitHub.

github.com


섹션6: 회원 관리 예제 - 웹 MVC 개발

1.1 회원 웹 기능 - 홈 화면 추가

 

홈 컨트롤러 추가

main/java/hongyeon_spring/controller/homeController

package hongyeon_spring.controller;

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

@Controller
public class HomeController {

    @GetMapping("/") //localhost:8080으로 처음 들어왔을 때 호출되는 메서드
    public String home(){
        return "home"; //home.html 호출
    } 
}
  • @GetMapping("/"): 처음 localhost:8080이 입력되었을 때 (/) 호출 되는 함수 매핑
  • return "home": home.html 파일로 연결

 

홈 화면 템플릿 추가

main/resources/templates/home.html

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

 

실행화면

 

서버는 클라이언트에서 요청이 들어오면 해당 요청에 매핑되는 컨트롤러가 있는지 스프링 컨테이너에서 먼저 확인하기에, 컨트롤러가 있으면 해당 요청에 해당하는 정적 파일이 있어도 컨트롤러를 선택한다

  • /에 해당하는 html 파일 index.html 파일이 main/resources/static/index.html에 있지만, 이는 정적 컨텐츠이므로 /에 매핑된 컨트롤러가 호출된다

 

1.2 회원 웹 기능 - 등록

사용자가 홈 화면에서 회원 가입을 누르면 a 태그에 연결된 /members/new 화면으로 이동한다

이에 매핑된 컨트롤러를 만들자

 

main/java/hongyeon_spring/controller/MemberController 클래스

@GetMapping("/members/new") //해당 url을 직접 쳐서 들어오는 방식을 get방식이라고 함, 조회할 때 사용
public String createForm() {
	return "members/createMemberForm";
}
  • GET 메소드: 데이터 정보 조회시 사용되는 HTTP 메소드
  • members 폴더의 createMemberForm.html 파일을 렌더링한다

 

resources/templates/members/createMemberForm.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
<!--    등록 버튼을 누르면 members/new로 post 방식으로 데이터가 전달됨 → url에 members/new이 요청 → 컨트롤러가 매핑된 메소드를 찾음-->
    <form action="/members/new" method="post">
        <div class="form-group">
            <label for="name">이름</label>
            <!-- input 태그의 name 속성은 서버로 넘어가는 데이터의 키값이 됨-->
            <input type="text" id="name" name="name" placeholder="이름을 입력하세요">
        </div>
        <button type="submit">등록</button>
    </form>
</div> <!-- /container -->
</body>
</html>
  • input 태그로 사용자에게 이름을 받는다
  • 이름은 name이라는 서버 전달 데이터 키와 "name"이라는 값에 저장된다
  • 사용자가 등록 버튼을 누르면 form에 저장된 이 이름의 값이 post 방식으로 /members/new에 전달된다
  • POST 메소드: 데이터 생성/변경 등 서버 상태를 변화시키는 데이터를 제출 시 사용되는 HTTP 메소드

main/java/hongyeon_spring/controller/MemberFrom 

package hongyeon_spring.controller;

public class MemberForm {

    //<input type="text" id="name" name="name" placeholder="이름을 입력하세요">에서
    // spring이 name이라는 키에 해당하는 값을 setName을 호출해서 넣어줌
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
  • /members/new 화면에서 데이터를 전달 받은 폼 객체의 클래스

 

main/java/hongyeon_spring/controller/MemberController 클래스

//<form action="/members/new" method="post">에서 방식이 post라 PostMapping 이용
@PostMapping("/members/new") //해당 페이지에서 데이터를 form에 넣어서 전달할 때 매핑되는 함수
public String create(MemberForm form){
	Member member = new Member();
	member.setName(form.getName());

	memberService.join(member);

	return "redirect:/"; //회원가입 끝나면 홈 화면으로 보냄
}
  • @PostMapping("/members/new"): /members/new 페이지에서 post 방식으로 form에 데이터를 담아서 /members/new를 호출할 때 매핑되는 함수
  • MemberForm form: 해당 페이지에서 전달된 폼 데이터를 담고 있는 객체를 매개변수로 전달 받음
  • 폼 데이터에 전달받은 값을 꺼내서 새로운 멤버로 만들고 memberservice.join함수로 회원가입 함
  • return "redirect:/": 홈 화면으로 리다이렉

1.3 회원 웹 기능 - 조회

사용자가 홈 화면에서 회원 목록을 누르면 a 태그에 연결된 /members 화면으로 이동한다

이에 매핑된 컨트롤러를 만들자

 

main/java/hongyeon_spring/controller/MemberController 클래스

@GetMapping("/members")
public String list(Model model){
	List<Member> members = memberService.findMembers();
	model.addAttribute("members", members); //키 "members", 값 members(리스트)으로 model에 데이터 담음
	return "members/memberList"; //해당 html 파일에 데이터를 model에 담아서 전달
}
  • Model model: 스프링이 자체적으로 Model 객체를 만들어 매개변수로 전달하며 이 함수를 호출
  • 컨트롤러에서 뷰(html파일)로 데이터를 전달하기 위해 사용하는 객체
  • 뷰 리졸버가 템플릿을 찾고 Thymeleaf가 이 데이터를 렌더링함
  • 내부적으로는 key-value 형태로 데이터를 저장하는 Map구조를 가지고 있음
  • model.addAttribute("members", members): "members"라는 키에 memberService.findMembers()로 받은 모든 멤버 객체들을 저장함
  • return "members/memberList": 해당 html 파일에 model 객체를 매개변수로 넘겨주면서 호출

 

resources/templates/members/memberList.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
    <div>
        <table>
            <thead>
            <tr>
                <th>#</th>
                <th>이름</th>
            </tr>
            </thead>
            <tbody>
            <!-- 전달 받은 model에서 키가 members인 데이터를 찾고 리스트니까 객체를 하나씩 꺼냄 -->
            <!-- 현재 레포지토리로 메모리를 사용 중이기에 서버를 내리면 저장된 데이터가 날라감 -->
            <!-- 따라서 데이터를 파일이나 db에 저장해야 함-->
            <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>
  • <tr th:each="member : ${members}">: 전달받은 model 객체에서 키가 members인 것을 찾고 for-each문 처럼 members 데이터가 리스트이니 하나씩 객체를 꺼내서 member로 이용
  • 지금 소스 코드에서 레포지토리를 메모리로 사용하고 있기 때문에 서버를 종료하면 메모리에 저장된 데이터도 전부 날라감
  • 따라서 레포지토리로 파일이나 db로 사용해서 서버가 종료되도 데이터는 보존되도록 해야함

실행 화면

 

렌더링 된 소스 코드

<!DOCTYPE HTML>
<html>
<body>
<div class="container">
    <div>
        <table>
            <thead>
            <tr>
                <th>#</th>
                <th>이름</th>
            </tr>
            </thead>
            <tbody>
            <!-- 전달 받은 model에서 키가 members인 데이터를 찾고 리스트니까 객체를 하나씩 꺼냄 -->
            <tr>
                <td>1</td>
                <td>spring1</td>
            </tr>
            <tr>
                <td>2</td>
                <td>spring2</td>
            </tr>
            </tbody>
        </table>
    </div>
</div> <!-- /container -->
</body>
</html>

섹션7: 스프링 DB 접근 기술

2.1 H2 데이터베이스 설치

https://www.h2database.com/

 

H2 Database Engine (redirect)

H2 Database Engine Welcome to H2, the free SQL database. The main feature of H2 are: It is free to use for everybody, source code is included Written in Java, but also available as native executable JDBC and (partial) ODBC API Embedded and client/server mo

www.h2database.com

해당 링크에서 다운로드 및 설치

설치된 폴더의 H2/bin 폴더에서 h2.bat 파일을 더블 클릭해서 실행

 

  • url로 test.mv.db 파일이 지정될 곳을 선택
  • 기본값으로 ~/test를 사용해도 된다
  • 비밀번호는 공백으로 설정

연결 후 다음 DDL로 DB에 member 테이블 생성

drop table if exists member CASCADE;
create table member
(
    id bigint generated by default as identity, #id값은 생성시 자동으로 증가
    name varchar(255),
    primary key (id)
);
  • 스프링 프로젝트에서도 sql 폴더를 만들어 ddl.sql 파일에 해당 sql문을 저장해둔다

 

2.2 순수 JDBC

 

build.gradle 파일의 dependencies 목록에 다음 두 라이브러리 추가

implementation 'org.springframework.boot:spring-boot-starter-jdbc' //자바와 db연결 - jdbc
runtimeOnly 'com.h2database:h2' //데이터베이스가 제공하는 클라이언트

 

resources/application.properties 파일에 스프링부트 데이터베이스 연결 설정 추가

spring.application.name=hongyeon-spring
spring.datasource.url=jdbc:h2:tcp://localhost/D:/GitHub/ECC-Backend-Study/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa

 

 

JDBC 레포지토리 구현

src/main/java/repository/JdbcMemberRepository 클래스

package hongyeon_spring.repository;

import hongyeon_spring.domain.Member;
import org.springframework.jdbc.datasource.DataSourceUtils;

import javax.sql.DataSource;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public class JdbcMemberRepository implements MemberRepository{

    //db에 붙기 위해선 DataSource가 필요
    //데이터베이스와 연결을 관리하는 객체로,db의 url, 아이디, 비번을 가지고 있으며 db와의 연결 통로인 Connection을 획득할 때 사용
    private final DataSource dataSource;

    //스프링이 생성한 DataSource 객체를 주입 받음
    //db 연결 정보가 바뀌어도 이 클래스의 코드는 수정할 필요가 없어짐
    public JdbcMemberRepository(DataSource dataSource) {
        this.dataSource = dataSource;

        //실제로 db와 연결된 Connection을 하나 가져옴, 데이터를 저장, 조회하는 메소드 내부에서 호출
        //매개변수로 sql문을 넣어서 db에 전달
        //dataSource.getConnection();
    }

    private Connection getConnection(){
        return DataSourceUtils.getConnection(dataSource); //트랙잭션 안걸리게 connection 똑같은걸 유지
    }

    @Override
    public Member save(Member member) {
        String sql = "insert into member(name) values(?)"; //실행할 쿼리문, 변수보다는 메소드 외부에 상수로 두는 것이 좋음

        Connection conn = null; //db와 연결된 물리적인 통로(세션) 관리
        PreparedStatement pstmt = null; //컴파일된 sql문을 담고 있는 객체, ?에 값을 바인딩하고 쿼리를 실행하는 역할
        ResultSet rs = null; //쿼리 실행 결과로 반환된 데이터 테이블을 가리키는 객체

        try{
            conn = getConnection(); //datasource를 통해 db와의 연결(Connection)을 가져옴
            //sql문 실행 준비, RETURN_GENERATED_KEYS은 db가 자동으로 생성해주는 id 값을 다시 받아오기 위함
            pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
            pstmt.setString(1, member.getName()); //첫번째 ? 자리에 저장할 회원의 이름을 넣음

            pstmt.executeUpdate(); //실제 db에 insert 쿼리를 날림, insert, update, delete, create, drop(DDL)을 실행할 때 사용
            rs = pstmt.getGeneratedKeys(); //db가 생성한 id 값들을 담은 걸 리턴받음

            //값이 존재하면 그 id를 member 객체에  세팅
            if(rs.next()){
                member.setId(rs.getLong(1)); //rs에서 첫 번째 칼럼(생성된 ID)를 읽어와 member객체 세팅
            } 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; //db와 연결된 물리적인 통로(세션) 관리
        PreparedStatement pstmt = null; //컴파일된 sql문을 담고 있는 객체, ?에 값을 바인딩하고 쿼리를 실행하는 역할
        ResultSet rs = null; //쿼리 실행 결과로 반환된 데이터 테이블을 가리키는 객체

        try{
            conn = getConnection();
            pstmt = conn.prepareStatement(sql);
            pstmt.setLong(1, id); //sql문의 첫번째 ?에 매개변수로 전달받음 id값을 넣음

            rs = pstmt.executeQuery(); //select 문 전용

            if(rs.next()){ //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 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);
        }
    }

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

    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){
                conn.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    private void close(Connection conn) throws SQLException{
        DataSourceUtils.releaseConnection(conn, dataSource);
    }
}
  • Datasource datasource: 데이터베이스와 연결을 관리하는 객체, db의 url, 아이디, 비번을 가지고 db와의 연결 통로인 Connection을 획득할 때 사용, final을 붙여서 수정되지 않게 관리
  • public JdbcMemberRepository(DataSource dataSource): 스프링이 생성한 DataSource 객체를 의존성 주입 받음
  • String sql: 실행할 sql문을 지정, 변수보다는 메소드 밖에 상수로 두는 것이 좋음. ?로 지정할 파라미터 표시
  • Connection conn: db와 연겨로딘 물리적인 통로(세션) 관리
  • PreparedStatement pstmt: 컴파일된 sql문을 담고 있는 객체, ?에 값을 바인딩하고 쿼리를 실행하는 역할
  • ResultSet rs: 쿼리 실행 결과로 반환된 데이터(테이블)을 가리키는 객체
  • conn = getConnection(): DataSource를 통해 db와의 연결(Connection)을 가져옴
  • pstmt = conn.prepareStatment(sql): sql문 실행 준비
  • Statement.RETURN_GENERATED_KEYS: db가 자동 생성해주는 id값을 임시로 저장해서 가져오기 위해 사용
  • pstmt.setString(1, member.getName()): 위에서 지정한 sql문의 첫 번째 ?자리에 member.getName()에서 가져온 값을 넣음
  • pstmt.executeQuery(): 쿼리 실행, select 문 전용
  • pstmt.executeUpdate(): 쿼리 실행, insert, update, delete, create, drop과 같은 ddl 실행 전용
  • rs = pstmt. executeQuery(): 쿼리 실행할 결과를 rs에 담음
  • rs = pstmt.getGeneratedKeys(): Statement.RETURN_GENERATED_KEYS로 저장한 id값을 여기서 가져와서 rs에 저장
  • rs.next(): rs는 처음에 실제 데이터가 아니라 첫 번째 행의 바로 위를 가리키고 있음 그래서 next()를 실행해야 데이터가 있는 첫 번째 행으로 커서 이동
  • rs.getLong(1): rs = pstmt.getGeneratedKeys()에서 저장된 1행 1열 테이블(id를 저장)에서 첫 번째 칼럼(id)를 읽어와 member 객체에 세팅
  • member.setId(rs.getLong("id")) : rs에서 칼럼이름이 id인 값을 찾아서 리턴하고 member 객체에 세팅
    member.setName(rs.getString("name")): rs에서 칼럼 이름이 name인걸 찾아서 리턴하고 member 객체에 세팅
  • Optional.of(member): 조회한 member가 없어서 null값일 수도 있으므로 optional로 한번 감싸서 리턴
  • close() 함수: 사용한 리소스는 매번 닫아줘야 함. 닫는 순서 ResultSet → PreparedStatement → Connection

 

SpringConfig 수정

package hongyeon_spring;

import hongyeon_spring.repository.JdbcMemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import hongyeon_spring.repository.MemberRepository;
import hongyeon_spring.repository.MemoryMemberRepository;
import hongyeon_spring.service.MemberService;

import javax.sql.DataSource;

@Configuration
public class SpringConfig {

	private DataSource dataSource;

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

	//스프링이 스프링 빈에 등록함
	@Bean
	public MemberService memberService() {
		return new MemberService(memberRepository());
	}
	//스프링이 스프링 빈에 등록함
	@Bean
	public MemberRepository memberRepository() {
		return new JdbcMemberRepository(dataSource);
	}
}
  • MemberRepository 메소드가 리턴하는 값을 MemoryMemberRepository에서 JdbcMemberRepository로 변경
  • 실제 MemberService 클래스는 전혀 수정하지 않아도 됨
  • 이렇게 내부를 수정해도 애플리케이션에는 아무 영향이 없는걸 자바의 다형성 실현이라 함

2.3 스프링 통합 테스트

src/test/java/service/MemberServiceIntergrationTest

package hongyeon_spring.service;

import hongyeon_spring.domain.Member;
import hongyeon_spring.repository.MemberRepository;
import hongyeon_spring.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

//테스트 실행시 트랙잭션을 먼저 실행, db에 데이터를 인서트 쿼리, 테스트가 끝나면 데이터를 롤백해줌
//db에 테스트 코드 데이터가 반영되지 않도록 해줌
@Transactional
class MemberServiceIntegrationTest {

    //테스트 코드는 필드 기반으로 autowired도 괜찮음
    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;

    @Test
    void 회원가입() {
        //given 주어진 값을
        Member member = new Member();
        member.setName("spring");

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

        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));//해당 메소드를 실행했을 때 예외가 발생해야 함
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

        //then
    }

}
  • 기본적으로 순수 자바 코드 테스트를 복사해서 사용
  • @SpringBootTest: 스프링 컨테이너에 테스틀 올려서 함께 실행한다
  • @Transactional: 테스트 케이스에 해당 어노테이션이 있으면 테스트 시작 전에 트랙잭션 시작, db에 인서트 쿼리 날림, 테스트가 끝나면 테스트 데이터들을 전부 롤백해준다. db에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다
  • 이러한 통합 테스트를 스프링을 함께 실행하므로 오래 걸림
  • 반대로 순수 자바 코드로 이루어진 테스트를 단위 테스트라고 하며 통합 테스트보다 훨씬 속도가 빠름
  • 단위 테스트를 잘 쪼개서 테스트하는 게 좋은 테스트임

2.4. 스프링 JdbcTemplate

 

  • 순수 JDBC와 동일한 환경
  • JDBC API의 많은 반복 코드를 대부분 제거해준다
  • 단 SQL은 직접 작성해야 한다

src/main/java/repository/JdbcTemplateMemberRepository

package hongyeon_spring.repository;

import hongyeon_spring.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) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    @Override
    public Member save(Member member) {
        //알아서 insert문 만듦
        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()); // memberRowMapper가 리스트로 반환해줌
    }

    private RowMapper<Member> memberRowMapper(){
        return new RowMapper<Member>() {
        //return (rs, rowNum) 형식으로 람다 이용 가능
            @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;
            }
        };
    }
}
  • 생성자가 하나면 Autowired 어노티에션 생략 가능
  • JdbcTemplate은 DataSoure를 주입받아서 생성자에 전달
  • jdbcTemplate.query("sql문", RowMapper_함수): 실행할 sql문과 sql문의 결과들을 처리해줄 함수 지정
  • RowMapper는 리턴될 데이터를 처리해준다

2.5 JPA

  • 기존의 반복 코드와 sql문도 전부 jpa가 직접 만들어서 실행해줌
  • sql과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임 전환 가능
  • 개발 생산성 높일 수 있음
  • JPA = 객체 + ORM 기술JPA를 쓰려면 항상 트랙잭션이 있어야함 
  • ORM = Object Relational Mapping, 객체와 관계형 데이터베이스의 테이블을 매핑
  • JPA는 인터페이스고 구현체로 Hibernate로 많이 사용됨 (오픈소스임)

build.gradle 파일의 dependencies JPA, h2 데이터베이스 관련 라이브러리 추가

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
  • 해당 라이브러리는 내부에 jdbc 관련 라이브러리를 포함하므로 기존의 jdbc와 연결하는 라이브러리는 지워도 된다

 

스프링 부트에 JPA 설정 추가

#jpa가 날리는 sql 볼 수 있음
spring.jpa.show-sql=true
#자동 테이블 생성 기능 (여기선 끔)
spring.jpa.hibernate.ddl-auto=none
  • show-sql: jpa가 날리는 sql문 출력
  • ddl-auto: jpa가 자동으로 테이블 생성하는 기능

 

JPA를 사용하기 위해선 엔티티 매핑을 해야 한다

src/main/java/domain/Member

package hongyeon_spring.domain;

import jakarta.persistence.*;

@Entity //jpa가 관리하는 entity가 됨
public class Member {

    //pk로 Id 매핑
    //이 pk는 db에서 자동으로 생성해줌 = identity 전략
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id; //고객이 정하는게 아니라 시스템이 정하는 고객아이디

    // @Column(name = "username"), db의 칼럼명이 username인것을 name에 매핑
    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;
    }
}
  • @Entity: jpa에 관리하는 엔티티 취급
  • @Id: pk 매핑
  • @GeneratedValue(strategy = GenerationType.Identity): 해당 pk는 db에서 자동 생성해주는 값임을 설정 = identity 전략
  • @Column(name = "username"): db의 칼럼명이 username인 것을 name에 매핑

 

src/main/java/repository/JpaMemberRepository

package hongyeon_spring.repository;

import hongyeon_spring.domain.Member;
import jakarta.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;
    }

    @Override
    public Member save(Member member) {
        em.persist(member);
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        Member member = em.find(Member.class, id); //조회할 타입, 식별할 pk
        return Optional.ofNullable(member);
    }

    @Override
    public Optional<Member> findByName(String name) {
        List<Member> result =  em.createQuery("select m from Member m where m.name = :name", Member.class)
                .setParameter("name", name)
                .getResultList();
        return result.stream().findAny();
    }

    @Override
    public List<Member> findAll() {
        //JPA 쿼리 언어 이용, 객체를 대상으로 쿼리를 날림, sql로 번역함, member 객체 자체를 select함
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }
}
  • EntityManager: Jpa에서 사용하는 객체
  • em.persist(member): member 객체를 db에 저장, 내부 자바 코드, sql은 jpa가 알아서 써줌
  • em.find(Member.class, id); //조회할 타입, 식별할 pk
  • JPA 쿼리 언어 - 객체를 대상으로 쿼리를 날리고 sql로 번역한다
  • select m from Member m: select문에서 m이라는 Member 객체 그 자체를 select함
  • select m from Member m where m.name = :name", Member.class: sql문에 name 매핑, 객체 타입 매핑

 

src/main/java/service/MemberService에 @Transactional 추가

@Transactional // JPA는 데이터 저장, 변경이 모두 트랙잭션 안에서 이뤄져야 함
public class MemberService {
  • 스프링은 해당 클래스의 메서드를 실행할 때 트랙잭션을 시작하고 메서드가 정상 종료되면 트랙잭션을 커밋한다
  • 만약 런타임 에러 발생시 롤백
  • JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행해야 한다

2.6 스프링 데이터 JPA

  • 기본적인 CRUD 메소드는 전부 다 제공됨. 따라서 따로 작성할 필요도 없음

src/main/java/repository/SpringDataJpaMemberRepository라는 인터페이스 생성

package hongyeon_spring.repository;

import hongyeon_spring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

//인터페이스가 인터페이스 받을 때는 extends, 다중 상속 가능
// 받을 객체의 타입, pk로 사용할 키의 타입
//JpaRepository를 받고 있으면 인터페이스만 구현해도 구현체를 알아서 만들어서 스프링빈에 자동으로 등록해줌
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
    @Override
    Optional<Member> findByName(String name);\
    //select m from Member m where name = ? 라는 JPQL를 알아서 짜줌, Sql로 번역되서 실행됨
}
  • 인터페이스가 인터페이스를 받을 때는 extends 이용, 다중 상속 가능
  • JpaRepository를 받는데 매개변수로 받을 객체의 타입과 pk로 사용할 키의 타입을 지정
  • JpaRepository를 받으면 인터페이스만 작성해도 구현체를 알아서 만들어서 스프링빈에 자동으로 등록해준다
  • 기본적인 CRUD는 전부 알아서 작성해주기에 인터페이스 차원에서도 작성해주지 않아도 된다
  • findByName처럼 공통화되지 않은 메소드의 인터페이스만 작성해주면되며 네이밍 규칙에 따라 알아서 그에 따른 구현체를 전부 작성해준다
  • 해당 메소드에 매핑되는 JPQL을 알아서 짜고 sql로 번역되어서 실행된다

 

 

src/main/java/SpringConfig 스프링 설정 변경

package hongyeon_spring;

import hongyeon_spring.repository.JdbcMemberRepository;
import hongyeon_spring.repository.JpaMemberRepository;
import jakarta.persistence.EntityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import hongyeon_spring.repository.MemberRepository;
import hongyeon_spring.repository.MemoryMemberRepository;
import hongyeon_spring.service.MemberService;

import javax.sql.DataSource;

@Configuration
public class SpringConfig {

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

	private final MemberRepository memberRepository;

	@Autowired // 생략 가능
	public SpringConfig(MemberRepository memberRepository) {
		this.memberRepository = memberRepository;
	}

	//스프링이 스프링 빈에 등록함
	@Bean
	public MemberService memberService() {
		return new MemberService(memberRepository);
	}
	//스프링이 스프링 빈에 등록함
//	@Bean
//	public MemberRepository memberRepository() {
//		//return new MemoryMemberRepository();
//		//return new JdbcMemberRepository(dataSource);
//		//return new JdbcMemberRepository(dataSource);
//		//return new JpaMemberRepository(em);

	}
  • 스프링 데이터 JPA가 SpringDataJpaMemberRepository를 스프링 빈으로 자동 등록해줌

 

 

스프링 데이터 JPA가 제공해주는 클래스

  • 기본적인 CRUD 구현체는 전부 제공하기에 인터페이스만 작성하면 됨
  • findByName(), findByEmail()처럼 메서드 이름 만으로 조회 기능 제공
  • 페이징 기능 자동 제공
  • 실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용하고 복잡한 동적 쿼리는 Querydsl이라는 라이브러리 사용
  • Querydsl를 사용하면 쿼리도 자바 코드로 안전하게 작성 가능, 동적 쿼리도 편하게 작성 가능
  • 이 조합으로 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리 이용 or 스프링 Jdbc Template 사용