| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- 유니티 안드로이드 빌드
- 주석 단축키
- 데이터베이스
- 유니티 에디터
- #AwsCloudClubs
- 티스토리챌린지
- 유니티 모듈
- 기술심화
- GitHub
- vscode
- 박스 모델
- javascript
- SQL
- 오블완
- 파이썬
- HeidiSQL
- 프론트엔드
- 유니티
- react
- 코드트리
- html
- 깃허브
- 웹 개발
- 학습회고
- 코드트리조별과제
- 깃허브뱃지
- 코딩테스트
- CSS
- #UMC #UMC10기 #UMC 블로그챌린지 #IT동아리
- route53
- Today
- Total
Hello It's good to be back ^_^
[ECC-백엔드 5팀] 9주차 스터디 본문
범위: 섹션 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 데이터베이스 설치
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 사용
'Study > Spring Boot' 카테고리의 다른 글
| [ECC-백엔드 5팀] 10주차 스터디 (0) | 2026.05.23 |
|---|---|
| [ECC-백엔드 5팀] 8주차 스터디 - 도메인, 레포지토리, 컨트롤러, 서비스, 테스트, 의존성 주입, 스프링 빈, 컴포넌트 스캔 (0) | 2026.05.07 |
| [ECC-백엔드 5팀] 7주차 스터디 - 스프링 입문, 정적 콘텐츠, MVC, API (0) | 2026.05.02 |
| [ECC-백엔드 5팀] 3주차 스터디 (0) | 2026.04.02 |
| [ECC-백엔드 5팀] 2주차 스터디 (0) | 2026.03.25 |
