1. 스프링 부트 프로젝트의 구조 이해하기
src/main/java 디렉터리 살펴보기
src/main/java 디렉터리는 자바 파일을 저장하는 공간
- com.mysite.sbb 패키지
- 컨트롤러는 URL 요청을 처리하고 폼은 사용자의 입력을 검증
- DTO, 엔티티, 서비스 파일은 데이터베이스 처리를 위한 파일
- sbb의 자바 파일을 저장하는 공간. (컨트롤러, 폼과 DTO, 데이터베이스 처리를 위한 엔티티, 서비스)
- SbbApplication.java 파일
- SbbApplication 클래스에는 반드시 @SpringBootApplication 애너테이션이 적용
src/main/resources 디렉터리 살펴보기
src/main/resources 디렉터리는 자바 파일을 제외한 HTML, CSS, Javascript, 환경설정 파일 등
을 저장하는 공간
- application.properties 파일
- 스프링부트의 환경설정 파일로 데이터베이스 등의 설정을 담당
- 데이터베이스 연결 정보, 포트 설정 등 다양한 환경 설정이 포함
- templates 디렉터리
- 템플릿 파일(.html)을 저장하는 공간(자바 코드를 삽입 가능)
- templates에는 SBB 게시판 서비스에 필요한 ‘질문 목록’, ‘질문 상세’ 등의 웹 페이지 구성
- 타임리프(Thymeleaf) 같은 템플릿 엔진을 사용하여 동적 HTML을 생성
- static 디렉터리
- CSS, Javascript, 이미지 파일(jpg 파일, png 파일) 등 정적 파일을 저장하는 공간
src/test/java 디렉터리 살펴보기
- src/test/java 디렉터리는 sbb 프로젝트에서 작성한 파일을 테스트하는 코드를 저장하는 공간
- JUnit과 스프링 부트의 테스트 도구를 사용하여 서버를 실행하지 않은 상태에서 코드를 src/main/java 디렉터리에 작성한 코드를 테스트
- JUnit은 테스트 코드를 작성하고, 작성한 테스트 코드를 실행할 때 사용하는 자바의 테스트 프레임워크
build.gradle 파일 살펴보기
- build.gradle은 그레이들이 사용하는 환경 파일
- 그레이들은 그루비(Groovy)를 기반으로 한 빌드 도구
- 프로젝트에 필요한 플러그인과 라이브러리를 설치하기 위한 내용을 작성
- 그루비는 그레이들 빌드 스크립트를 작성하는 데 사용하는 스크립트 언어, 문법이 간결하고 가독성이 높다.
- 빌드 도구는 소스 코드를 컴파일하고 필요한 라이브러리를 내려받을 때 사용
- SBB 프로젝트를 완성하면 단 한 개의 jar 파일로 패키징하여 서버에 배포할 수 있는데 이때에도 역시 빌드 도구를 사용한다.
2. 간단한 웹 프로그램 만들기
URL 매핑과 컨트롤러 이해하기
URL 매핑이란 URL과 컨트롤러의 메서드를 일대일로 연결하는 것을 말함.
컨트롤러의 메서드에 @GetMapping 또는 @PostMapping과 같은 애너테이션을 적용하면 해당 URL과 메서드가 연결된다.
컨트롤러 만들어서 URL 매핑하기
웹 브라우저와 같은 클라이언트의 요청이 발생하면 서버 역할을 하는 스프링 부트가 응답
package com.mysite.sbb;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class MainController {
@GetMapping("/sbb")
// URL 요청에 대한 응답으로 문자열을 리턴하라는 의미
@ResponseBody
public String index(){
return "index";
}
}
- MainController 클래스에 @Controller 애너테이션을 적용하면 MainController 클래스는 스프링 부트의 컨트롤러가 된다.
- index 메서드의 @GetMapping 애너테이션은 요청된 URL(./sbb)과의 매핑을 담당한다.
- 브라우저가 URL을 요청하면 스프링 부트는 요청 페이지와 매핑되는 메서드를 찾아 실행한다.
- @GetMapping에는 http://localhost:8080과 같은 도메인명과 포트는 적지 않는다.
- 오류가 발생하는 이유
- 원래 URL과 매핑된 메서드는 결괏값을 리턴해야 하는데 아무 값도 리턴하지 않아 오류가 발생
- 이번에는 404가 아닌 500 오류 코드
- ⇒ 스프링 부트는 웹 브라우저로부터 http://localhost:8080/sbb 요청이 발생하면 /sbb URL과 매핑되는 index 메서드를 MainController 클래스에서 찾아 실행한다.
3. JPA로 데이터베이스 사용하기
목표 : 방문자들이 질문과 답변을 남길 수 있는 게시판 서비스
- SBB 게시판의 사용자가 질문이나 답변을 작성하면 데이터 생성
- 이러한 데이터를 저장, 조회, 수정
문제점: 데이터베이스는 자바를 이해하지 못함.
⇒ 여기서 ORM(Object Relational Mapping)이라는 도구를 사용(SQL 작성 X)
ORM과 JPA 이해하기
ORM이란?
- ORM은 SQL을 사용하지 않고 데이터베이스를 관리할 수 있는 도구
- ORM은 데이터베이스의 테이블을 자바 클래스로 만들어 관리 가능
EX. 아래와 같이 테이블이 있다고 해보자.
id subject content
1 | 안녕하세요 | 가입 인사드립니다 ^^ |
2 | 질문 있습니다 | ORM이 궁금합니다 |
… | … |
SQL 쿼리문
insert into question(id, subject, content) values (1, ‘안녕하세요’, ‘가입 인사드립니다 ^^’);
insert into question(id, subject, content) values(2, ‘질문 있습니다’, ‘ORM이 궁금합니다’);
ORM을 사용한 자바 코드
// 자바 클래스이자 엔티티
Question q1 = new Question();
q1.setId(1);
q1.setSubject("안녕하세요");
q1.setContent("가입 인사드립니다 ^^");
this.questionRepository.save(q1);
Question q2 = new Question();
q2.setId(1);
q2.setSubject("질문 있습니다");
q2.setContent("ORM이 궁금합니다");
this.questionRepository.save(q2);
- 엔티티란?
⇒ 엔티티는 데이터베이스의 테이블과 매핑되는 자바 클래스
- ORM의 장점?
- ORM을 이용하면 MySQL, 오라클 DB, MS SQL과 같은 DBMS의 종류에 관계없이 일관된 자바 코드를 사용
- 코드 내부에서 안정적인 SQL 쿼리문을 자동으로 생성해줘 개발자가 달라도 통일된 쿼리문을 작성할 수 있고, 오류 발생률도 줄일 수 있음
- DBMS란?
- (즉, DB는 데이터를 담은 통, DBMS는 이 통을 관리하는 소프트웨어)
JPA란?
JPA(JAVA Persistence API) ⇒ ORM의 기술 표준
- JPA를 이용하여 데이터베이스를 관리
- JPA는 인터페이스 모음으로 이 인터페이스를 구현한 실제 클래스가 필요하다. (hibernate)
- JPA + 하이버네이트 조합
- H2 데이터베이스 설치하기1. Java 설치 확인:BashJava가 설치되어 있지 않거나 오래된 버전이라면, Oracle 웹사이트 또는 OpenJDK 웹사이트에서 최신 버전의 Java를 다운로드하여 설치하세요.H2 데이터베이스 공식 웹사이트에서 최신 버전의 JAR 파일을 다운로드합니다.
웹사이트의 "Download" 섹션에서 "h2-yyyy-mm-dd.jar" 형태의 파일을 다운로드합니다. (yyyy-mm-dd는 날짜를 나타냅니다.)다운로드한 JAR 파일이 있는 디렉토리로 이동한 후, 명령 프롬프트 또는 터미널에서 다음 명령어를 실행하여 H2 콘솔을 실행합니다.java -jar h2-yyyy-mm-dd.jar이 명령어를 실행하면 웹 브라우저가 자동으로 열리면서 H2 콘솔 페이지가 나타납니다.H2 콘솔 페이지에서 데이터베이스 접속 설정을 할 수 있습니다. 일반적으로 다음과 같은 설정을 사용합니다.
- JDBC URL: jdbc:h2:./test (현재 디렉토리에 test.mv.db 파일로 데이터베이스를 생성합니다.) 또는 jdbc:h2:mem:test (메모리 기반 데이터베이스를 생성합니다.)
- User Name: sa (기본 사용자 이름)
- Password: (비워두거나 원하는 비밀번호를 설정할 수 있습니다.)
- Zip 파일 압축 해제: H2 웹사이트에서 Zip 파일을 다운로드하여 압축을 해제하면, 실행 스크립트 (예: h2.sh, h2.bat)를 통해 H2를 실행할 수도 있습니다.
- Maven 또는 Gradle 의존성 추가: Java 프로젝트에서 H2 데이터베이스를 사용하려면, Maven 또는 Gradle 빌드 파일에 H2 의존성을 추가할 수 있습니다.XMLGradle (build.gradle):runtimeOnly 'com.h2database:h2:최신 버전'
- ( 최신 버전은 H2 데이터베이스의 최신 버전을 명시해야 합니다.)
- Gradle
- <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>최신 버전</version> <scope>runtime</scope> </dependency>
- Maven (pom.xml):
- H2 데이터베이스는 주로 개발 및 테스트 환경에서 사용되는 임베디드 데이터베이스입니다. 프로덕션 환경에서는 일반적으로 MySQL, PostgreSQL, Oracle 등과 같은 더 강력한 데이터베이스를 사용하는 것이 좋습니다.
- 파일 기반 데이터베이스 (jdbc:h2:./test)를 사용하는 경우, 데이터베이스 파일이 저장되는 위치를 잘 관리해야 합니다.
- 추가 설치 방법:
- 4. H2 콘솔 접속 설정:
- ( h2-yyyy-mm-dd.jar는 다운로드한 파일 이름으로 바꿔주세요.)
- Bash
- 3. H2 데이터베이스 실행 (콘솔 모드):
- 2. H2 데이터베이스 다운로드:
- java -version
- H2 데이터베이스는 Java 기반으로 실행되므로, 먼저 시스템에 Java가 설치되어 있는지 확인해야 합니다. 명령 프롬프트 또는 터미널을 열고 다음 명령어를 실행하여 Java 버전을 확인합니다.
- H2 데이터베이스를 설치하는 방법은 여러 가지가 있지만, 가장 일반적인 방법은 JAR 파일을 다운로드하여 실행하는 것입니다. 아래에 자세한 설치 단계를 안내해 드리겠습니다.
JPA 환경 설정하기
- build.gradle 파일 수정
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
- implementation이란?
- build.gradle파일에서 작성한 implementation은 필요한 라이브러리 설치를 위해 가장 일반적으로 사용하는 설정
- implementation은 해당 라이브러리가 변경되더라도 모든 모듈을 컴파일하지 않고 관련이 있는 모듈만 컴파일하므로 리빌드(rebuild)하는 속도가 빠르다
- application.properties 파일 수정
#JPA
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
- spring.jpa.properties.hibernate.dialect: 스프링 부트와 하이버네이트를 함께 사용할 때 필요한 설정 항목. 표준 SQL이 아닌 하이버네이트만의 SQL을 사용할 때 필요한 항목
- spring.jpa.hibernate.ddl-auto: 엔티티를 기준으로 데이터의 테이블을 생성하는 규칙
- spring.jpa.hibernate.ddl-auto의 규칙
- none: 엔티티가 변경되도 데이터베이스를 변경하지 않는다.
- update: 엔티티의 변경된 부분만 데이터베이스에 적용한다.
- validate: 엔티티의 테이블 간에 차이점이 있는지 검사만 한다.
- create: 스프링 부트 서버를 시작할 때 테이블을 모두 삭제한 후 다시 생성
- create-drop: create와 동일하지만 스프링 부트 서버를 종료할 때도 테이블을 모두 삭제
4. 엔티티로 테이블 매핑하기
데이터베이스 구성 요소 살펴보기
데이터베이스는 2차원 표 형태로 저장하고 구성
테이블(table): 표 형태의 데이터 저장 공간(가로줄(행:row)과 세로줄(열:column) 형태로 구성)
기본키(primary key): 테이블의 데이터가 중복되어 저장되지 않게 한다
⇒ 어떤 열을 기본키로 설정하면 해당 열에는 동일한 값을 저장하지 못함
엔티티 속성 구성하기
- SBB는 질문과 답변을 할 수 있는 게시판 서비스
- SBB의 질문과 답변 데이터를 저장할 데이터베이스 테이블과 매핑되는 질문 + 답변 엔티티
만들어야 할 질문(Question)과 답변(Answer) 엔티티에는 어떤 속성이 필요할까?
⇒ 질문 엔티티(질문의 ‘제목’과 ‘내용’)
속성 이름 설명
id | 질문 데이터의 고유 번호 |
subject | 질문 데이터의 제목 |
content | 질문 데이터의 내용 |
createDate | 질문 데이터를 작성한 일시 |
- 답변 엔티티
속성 이름 설명
id | 답변 데이터의 고유 번호 |
question | 질문 데이터 |
content | 답변 데이터의 내용 |
createDate | 답변 데이터를 작성한 일시 |
질문 엔티티 만들기
package com.mysite.sbb;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.List;
@Getter
@Setter
@Entity
public class Question {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(length = 200)
private String subject;
@Column(columnDefinition = "TEXT")
private String content;
private LocalDateTime createDate;
@OneToMany(mappedBy = "question", cascade = CascadeType.REMOVE)
private List<Answer> answerList;
}
- @Id 에너테이션
- 왜?
- id 속성을 기본키로 지정한 이유는 id속성의 고유 번호들은 엔티티에서 각 데이터들을 구분하는 유효한 값(중복 x)
- id 에너테이션은 id 속성을 기본키로 지정.
- @GeneratedValue 애너테이션
- @GeneratedValue 애너테이션을 적용하면 데이터를 저장할 때 해당 속성에 값을 일일이 입
력하지 않아도 자동으로 1씩 증가하여 저장
→ strategy = GenerationType.IDENTITY는 고유한 번호를 생성하는 방법을 지정하는 부분
→ GenerationType.IDENTTITY는 해당 속성만 별도로 번호가 차례대로 늘어날 때 사용
→ strategy 옵션을 생략한다면 @GeneratedValue 애너테이션의 지정된 모든 속성에 번호를
차례대로 생성하므로 순서가 일정한 고유한 번호를 가질 수 없음
- @Column 애너테이션
- length: 열의 길이
- columnDefinition : 열의 데이터의 유형이나 성격
- 엔티티의 속성은 @Column 애너테이션을 사용하지 않더라도 테이블의 열로 인식한다.@Transient 애너테이션은 엔티티의 속성을 테이블의 열로 만들지 않고 클래스의 속성 기능으로만 사용한다.
- 테이블의 열로 인식하고 싶지 않다면 @Transient 애너테이션을 사용한다.
- 엔티티의 속성은 테이블의 열 이름과 일치하는데 열의 세부 설정을 위해 @Column 애너테이션을 사용한다.
- 엔티티의 속성 이름과 테이블의 열 이름의 차이를 알아보자
- Question 엔티티에서 작성 일시에 해당하는 createDate 속성의 이름은 데이터베이스의 테이블에서는 create_date라는 열 이름으로 설정
- 즉, createDate처럼 카멜 케이스(camel case) 형식의 이름은 create_date처럼 모두 소문자로 변경되고 언더바로 구분되어 데이터베이스의 열 이름이 된다.
- 엔티티를 만들 때 Setter 메서드는 사용하지 않는다
- 일반적으로 엔티티를 만들 때에는 Setter 메서드를 사용하지 않기를 권한다.
- → 엔티티는 데이터베이스와 바로 연결되므로 데이터를 자유롭게 변경할 수 있는 Setter 메
답변 엔티티 만들기
Answer.java
package com.mysite.sbb;
import org.springframework.data.annotation.CreatedDate;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
@Getter
@Setter
@Entity
public class Answer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(columnDefinition = "TEXT")
private String content;
@CreatedDate
private LocalDateTime createDate;
@ManyToOne
private Question question;
}
- 질문의 제목을 알고 싶으면 answer.getQuestion.getSubject()를 사용해 접근
- question 속성만 추가하면 안되고 질문 엔티티와 연결된 속성이라는 것을 답변 엔티티에 표시해야 한다 ⇒ ManyToOne 애너테이션
- ManyToOne을 사용하는 이유
- 게시판 서비스에는 하나의 질문에 답변이 여러 개가 달릴 수 있다.
- 답변은 Many(많은 것)가 되고 질문은 One(하나)이 된다. (N:1)
- @ManyToOne 애너테이션을 설정하면 Answer(답변) 엔티티의 question 속성과 Question(질문) 엔티티가 서로 연결된다(실제 데이터베이스에서는 외래키(foreign) 관계가 생성)
- 외래키란? 테이블과 테이블 사이의 관계를 구성할 때 연결되는 열
반대로 질문에서 답변을 참조하는 경우?
질문 하나에 답변은 여러 개이므로 Question 엔티티에 추가할 Answer 속성은 List 형태로 구성
Question.java
package com.mysite.sbb;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.List;
@Getter
@Setter
@Entity
public class Question {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(length = 200)
private String subject;
@Column(columnDefinition = "TEXT")
private String content;
private LocalDateTime createDate;
@OneToMany(mappedBy = "question", cascade = CascadeType.REMOVE)
private List<Answer> answerList;
}
- Answer 객체들로 구성된 answerList를 Question 엔티티의 속성으로 추가
- 질문에서 답변을 참조하려면 question.getAnswerList()를 호출
- @OneToMany 애너테이션에 사용된 mappedBy는 참조 엔티티의 속성명을 정의
- Answer 엔티티에서 Question 엔티티를 참조한 속성인 question을 mappedBy로 전달
- CascadeType.REMOVE
- 게시판 서비스에서는 질문 하나에 답변 여러개
- 보통 게시판 서비스에서는 질문을 삭제하면 그에 달린 답변들도 함께 삭제 ⇒ SBB도 질문을 삭제하면 모두 삭제되도록 REMOVE 활용
5. 리포지터리로 데이터베이스 관리하기
엔티티만으로는 테이블의 데이터를 저장, 조회, 수정, 삭제 등을 할 수 없다.
⇒ 데이터를 관리하려면 데이터베이스와 연동하는 JPA 리포지터리가 반드시 필요하다
리포지터리 생성하기
엔티티가 데이터베이스 테이블을 생성했다면, 리포지터리는 이와 같이 생성된 데이터베이스 데이터들을 저장, 조회, 수정, 삭제 등을 할 수 있도록 도와주는 인터페이스
→ 이 때 리포지토리는 테이블에 접근하고, 데이터를 관리하는 메서드를 제공한다.
QuestionRepository.java
package com.mysite.sbb;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface QuestionRepository extends JpaRepository<Question, Integer> {
}
- QuestionRepository 인터페이스를 리포지터리로 만기 위해 JpaRepository 인터페이스를 상속한다.
- JpaRepository는 JPA가 제공하는 인터페이스 중 하나로 CRUD 작업을 처리하는 메서드들을 이미 내장하고 있어 데이터 관리 작업을 좀 더 편리하게 처리할 수 있다.
- JpaRepository<Question, Intger>는 Question 엔티티로 리포지터리를 생성한다는 의미 추가
AnswerRepoitory.java
package com.mysite.sbb;
import org.springframework.data.jpa.repository.JpaRepository;
public interface AnswerRepository extends JpaRepository<Answer, Integer> {
}
JUnit 설치하기
리포지터리를 이용하여 데이터를 저장하려면 질문을 등록하는 화면과 사용자가 입력한 질문 관련 정보를 저장하는 컨트롤러, 서비스 파일 등이 필요하다.
JUnit을 사용하면 이러한 프로세스를 따르지 않아도 리포지터리만 개별적으로 실행해 테스트
build.gradle 추가
testImplementation 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
질문 데이터 저장하기
package com.mysite.sbb;
import java.time.LocalDateTime;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests{
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa(){
// 자바 클래스이자 엔티티
Question q1 = new Question();
q1.setId(1);
q1.setSubject("안녕하세요");
q1.setContent("가입 인사드립니다 ^^");
this.questionRepository.save(q1);
Question q2 = new Question();
q2.setId(1);
q2.setSubject("질문 있습니다");
q2.setContent("ORM이 궁금합니다");
this.questionRepository.save(q2);
}
- @SpringBootTest 애너테이션은 SbbApplicationTests 클래스가 스프링 부트의 테스트 클래스임을 의미
- 질문 엔티티의 데이터를 생성할 때 리포지터리가 필요하므로 @Autowired 애너테이션을 통해 스프링의 ‘의존성 주입(DI)’이라는 기능을 사용해 QuestionRepository의 객체를 주입
- @Test 애너테이션은 testJpa 메서드가 테스트 메서드임을 나타낸다.
- @Autowired 애너테이션을 더 알아보자
- 테스트 코드를 살펴보면 questionRepository 변수는 선언만 되어 있고 그 값이 비어 있다.
⇒ 순환 참조 문제와 같은 이유로 개발 시 생성자를 통한 객체 주입 방식을 권장한다.2. @Autowired 애너테이션을 해당 변수에 적용하면 스프링 부트가 questionRepository 객체를 자동으로 만들어 주입한다. 3. 객체를 주입하는 방식에는 @Autowired 애너테이션을 사용하는 것 외에 Setter 메서드 또는 생성자를 사용하는 방식이 있다.
- 4. 하지만 테스트 코드의 경우 JUnit이 생성자를 통한 객체 주입을 지원하지 않아 테스트 코드 작성 시에만 @Autowired를 사용하고 실제 코드 작성 시에는 생성자를 통한 객체 주입 방식을 사용
질문 데이터 조회하기
findAll 메서드
SbbApplicationTests.java
package com.mysite.sbb;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.List;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa(){
List<Question> all = this.questionRepository.findAll();
assertEquals(2, all.size());
Question q = all.get(0);
assertEquals("sbb가 무엇인가요?", q.getSubject());
}
}
- 앞서 2개의 질문 데이터를 저장했기 때문에 데이터 사이즈는 2가 되어야 한다.
- 데이터 사이즈가 2인지 확인한기 위해 JUnit의 assertEquals 메서드를 사용하는데, 이 메서드는 테스트에 예상한 결과와 실제 결과가 동일한지를 확인하는 목적으로 사용한다.
- 즉, JPA 또는 데이터베이스에서 데이터를 올바르게 가져오는지를 확인하려는 것이다.
findById
SbbApplicationTests.java
package com.mysite.sbb;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.List;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa(){
Optional<Question> oq = this.questionRepository.findById(1);
if(oq.isPresent()){
Question q = oq.get();
assertEquals("sbb가 무엇인가요?", q.getSubject());
}
}
- id값으로 데이터를 조회하기 위해서는 리포지터리의 findById 메서드를 사용해야 한다.
- findById로 호출한 값이 존재할 수도 있고, 존재하지 않을 수도 있어서 타입으로 Optional을 사용(Question x)
- Optional은 그 값을 처리하기 위한(null 값을 유연하게 처리하기 위한) 클래스
- isPresent()를 통해 값이 존재하는 것을 확인하고, get() 메서드를 통해 실제 Question 객체의 값을 얻는다.
findBySubject 메서드
리포지터리는 findBySubject 메서드를 기본적으로 제공 x.
QuestionRepository.java
package com.mysite.sbb;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface QuestionRepository extends JpaRepository<Question, Integer> {
List<Question> findBySubject(String subject);
}
SbbApplicationTests.java
package com.mysite.sbb;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.List;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa(){
Question q = this.questionRepository.findBySubject("sbb가 무엇인가요?");
assertEquals(1, q.getId());
}
- 인터페이스에 findBySubject라는 메서드를 선언만 하고 구현하지 않았는데 실행?findBy + 엔티티의 속성명과 같은 리포지터리의 메서드를 작성하면 입력한 속성의 값으로 데이터를 조회
- JPA에 레포지터리의 메서드명을 분석하여 쿼리를 만들고 실행하는 기능이 있기 때문에 가능
⇒ 실제 데이터베이스에서 어떤 쿼리문이 실행되는지 살펴보자
application.properties
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.show_sql=true
findBySubjectAndContent 메서드
- SQL을 활용해 데이터베이스에서 두 개의 열(엔티티의 속성)을 조회하기 위해 AND 연산자 활용
QuestionRepository.java
package com.mysite.sbb;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface QuestionRepository extends JpaRepository<Question, Integer> {
List<Question> findBySubject(String subject);
List<Question> findBySubjectAndContent(String subject, String content);
}
SbbApplicationTests.java
package com.mysite.sbb;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.List;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa(){
Question q = this.questionRepository.findBySubject("sbb가 무엇인가요?", "sbb에 대해서 알고 싶습니다.");
assertEquals(1, q.getId());
}
⇒ 데이터베이스를 살펴보면 where 문에 and 연산자가 사용되어 subject와 content 열을 조회하는 것을 확인할 수 있다. (리포지터리의 메서드명은 쿼리문의 where 조건을 결정하는 역할)
- 응답 결과가 여러 건인 경우에는 리포지터리 메서드의 리턴 타입을 List<Question>으로 작성
findBySubjectLike 메서드
- subject 열 값들 중에 특정 문자열을 포함하는 데이터를 조회해 보자.
- SQL에서는 특정 문자열을 포함한 데이터를 열에서 찾을 때 Like를 사용
QuestionRepository.java
package com.mysite.sbb;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface QuestionRepository extends JpaRepository<Question, Integer> {
List<Question> findBySubject(String subject);
List<Question> findBySubjectAndContent(String subject, String content);
List<Question> findBySubjectLike(String subject);
}
SbbApplicationTests.java
package com.mysite.sbb;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.List;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa(){
List<Question> qList = this.questionRepository.findBySubjectLike("sbb%");
Question q = qList.get(0);
assertEquals("sbb가 무엇인가요?", q.getSubject());
}
질문 데이터 수정하기
package com.mysite.sbb;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.Optional;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa(){
Optional<Question> oq = this.questionRepository.findById(2);
Assertions.assertTrue(oq.isPresent());
Question q = oq.get();
q.setSubject("수정된 제목");
}
}
⇒ 변경된 질문을 데이터베이스에 저장하기 위해 this.questionRepository.save(q)와 같이 사용함.
질문 데이터 삭제하기
package com.mysite.sbb;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.Optional;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa(){
// 리포지터리의 count 메서드는 테이블 행의 개수를 리턴
assertEquals(2, this.questionRepository.count());
Optional<Question> oq = this.questionRepository.findById(1);
Assertions.assertTrue(oq.isPresent());
Question q = oq.get();
this.questionRepository.delete(q);
assertEquals(1, this.questionRepository.count());
}
}
답변 데이터 저장하기
package com.mysite.sbb;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.Optional;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Autowired
private AnswerRepository answerRepository;
@Test
void testJpa(){
// question 열에 데이터를 생성하려면 질문 데이터를 조회해야 한다.
Optional<Question> oq = this.questionRepository.findById(1);
assertTrue(oq.isPresent());
Question q = oq.get();
Answer a = new Answer();
a.setContent("네 자동으로 생성됩니다.");
// 답변 데이터의 question 속성에 질문 데이터를 대입해
// 답변 데이터를 생성하려면 Question 객체 q가 필요하다
a.setQuestion(q);
// 답변 입력 시간을 입력한다
a.setCreateDate(LocalDateTime.now());
}
}
- 질문 데이터를 저장할 때와 마찬가지로 답변 데이터를 저장할 때에도 리포지터리(AnswerRepository)가 필요하므로 AnswerRepository의 객체를 @Autowired를 통해 주입.
- questionRepository의 findById 메서드를 통해 id가 2인 질문 데이터를 가져와 답변의 question 속성에 대입해 답변 데이터를 생성
답변 데이터 조회하기
답변 엔티티도 id 속성이 기본키이므로 값이 자동으로 생성된다
package com.mysite.sbb;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.Optional;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa(){
Optional<Answer> oa = this.answerRepository.findById(1);
assertTrue(oa.isPresent());
Answer a = oa.get();
assertEquals(2, a.getQuestion().getId());
}
}
답변 데이터를 통해 질문 데이터 찾기 vs 질문 데이터를 통해 답변 데이터 찾기
앞에서 살펴본 답변 엔티티의 question 속성을 이용하면 다음과 같은 메서드를 사용해 ‘답변에 연결된 질문’에 접근할 수 있다. (a.getQuestion())
⇒ 답변에 연결된 질문 데이터를 찾는 것은 Answer 엔티티에 question 속성이 이미 정의되어 있어 쉽다.
package com.mysite.sbb;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa(){
Optional<Question> oq = this.questionRepository.findById(2);
assertTrue(oq.isPresent());
Question q = oq.get();
List<Answer> answerList = q.getAnswerList();
assertEquals(1, answerList.size());
assertEquals("네 자동으로 생성됩니다.", answerList.get(0).getContent());
}
}
- 오류 발생( DB 세션이 끊긴다)
⇒ 그 이후에 실행되는 q.getAnswerList() 메서드(Question 객체로부터 answer 리스트를 구하는 메서드)는 세션이 종료되어 오류가 발생한다. answerList는 앞서 q 객체를 조회할 때가 아니라 q.getAnswerList() 메서드를 호출하는 시점에 가져와 문제 발생
- 이렇게 데이터를 필요한 시점에 가져오는 방식을 지연(Lazy) 방식이라고 한다. 이와 반대로 q 객체를 조회할 때 미리 answer 리스트를 모두 가져오는 방식을 즉시(Eager) 방식이라고 한다. @OneToMany, @ManyToOne 애너테이션의 옵션으로 fetch-Fetch Type.Lazy 또는 fetchType.Eager처럼 가져오는 방식을 설정할 수 있다.
- 사실 이러한 문제들은 테스트 코드에서만 발생한다. 실제 서버에서 JPA 프로그램들을 실행할 때는 DB 세션이 종료되지 않아 이와 같은 오류가 발생하지 않는다.
- 어떻게 방지할 수 있을까? ⇒ @Transactional 애너테이션을 사용
'Spring Boot' 카테고리의 다른 글
Spring 5주차(회원 가입 기능 구현하기) (1) | 2025.06.01 |
---|---|
Spring 4주차(~스프링 시큐리티) (0) | 2025.06.01 |
Spring 3주차(게시판 기능 수정하기) (0) | 2025.05.11 |
Spring 2주차(~2-10 상세페이지 만들기) (1) | 2025.04.29 |