Jump to Spring Boot - 1

현곤·2024년 12월 25일

h2 database

build.gradle

runtimeOnly 'com.h2database:h2'

properties

spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.datasource.url=jdbc:h2:~/local
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

db 파일

touch db_dev.mv.db

gitignore

### Custom ###
db_dev.mv.db
db_dev.trace.db

JPA

build.gradle

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

properties

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update

Spring.jpa.hibernate.ddl-auto

이걸 update 로 설정했는데 또 다른 설정값이 있다

  • none : 엔티티가 변경되더라도 데이터베이스를 변경 X

  • update : 엔티티의 변경된 부분만 데이터베이스에 적용

  • validate : 엔티티와 테이블 간에 차이점이 있는지 검사

개발 환경에서는 보통 update

운영 환경에서는 none 또는 validate 사용


테이블 매핑

게시판은 사용자가 질문을 남기고 답변을 받을 수 있는 웹 서비스

서비스를 제공하기 위해서는 사용자가 입력한 질문을 저장,

질문의 제목과 내용을 담을 수 있는 항목이 필요

질문의 제목내용 등을 엔티티의 속성으로 추가해야 함


Question

id (고유 번호)

subject (제목)

content (내용)

createDate (작성한 날짜)


  • Question
@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;
}

Answer

id (고유 번호)

content (내용)

createDate (작성한 날짜)

Question (질문 엔티티 참조 속성)


@Getter
@Setter
@Entity
public class Answer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(columnDefinition = "TEXT")
    private String content;

    private LocalDateTime createDate;

    private Question question;
}

그런데 답변을 통해 질문의 제목을 알고 싶다면

answer.getQuestion().getSubject << 를 사용해 접근할 수 있는데

Question 속성만 추가하는 것이 아닌,

질문 엔티티와 연결된 속성이라는 것을 답변 엔티티에 표시해야 함

@ManyToOne 어노테이션을 추가해 질문 엔티티와 연결

그냥 Question 위에다 어노테이션 추가하기

@ManyToOne
private Question question;

하나의 질문에 답변은 여러 개가 달릴 수 있는데

답변은 Many 질문은 One, N : 1 관계를 나타냄


반대로 Question 에서 Answer 참조하는 방법은

@OneToMany 를 사용하고

질문은 하나, 답변은 여러개니까 Answer 을 List 형태로 구성

@OneToMany(mappedBy = "question", cascade = CascadeType.REMOVE)
    private List<Answer> answerList;

이제 질문에서 답변을 참조하려면 question.getAnswerList() 를 호출하면 됨

@OneToMany 어노테이션에 사용된 mappedBy 는 참조 엔티티의 속성명을 정의

Answer 엔티티에서 Question 엔티티를 참조한 속성인
questionmappedby 에 전달해야 한다.

Answer 엔티티가 Question 엔티티와 연관되어 있고,
Answer 엔티티 안에 question 이라는 변수가 Question 을 참조하고 있다면?
Question 엔티티에서 @OneToMany 를 설정할 때 mappedBy = "question"

이렇게 하면 두 엔티티가 서로 어떤 관계인지 JPA에게 알려줄 수 있다.


Question, Answer Repository

Repository

public interface QuestionRepository extends JpaRepository<Question, Integer> {
}
public interface AnswerRepository extends JpaRepository<Answer, Integer> {
}

JUnit (build.gradle)

testImplementation 'org.junit.jupiter:junit-jupiter' 
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

코끼리 눌러주기

AplicationTest

@SpringBootTest
class PracticeApplicationTests {

	@Autowired
	private QuestionRepository questionRepository;

@Test
	void testJpa() {
		Question q1 = new Question();
		q1.setSubject("sbb가 뭔고?");
		q1.setContent("sbb 궁금하다");
		q1.setCreateDate(LocalDateTime.now());
		this.questionRepository.save(q1);

		Question q2 = new Question();
		q2.setSubject("스프링부트 모델 질문인데");
		q2.setContent("id 자동으로 생성됨?");
		q2.setCreateDate(LocalDateTime.now());
		this.questionRepository.save(q2);
	}

@SpringBootTest 어노테이션은 이 클래스가 테스트임을 의미

질문 엔티티의 데이터를 생성할 때 리포지터리가 필요함

그래서 @Autowired의존성 주입이라는 기능을 사용해

QuestionRepository 객체 주입

스프링의 의존성 주입(DI, Dependency Injection)
스프링이 객체를 대신 생성하여 주입하는 기법

@Autowired

QuestionRepository 변수는 선언만 되어있고 값은 없는 상태

@Autowired << 어노테이션을 변수에 적용하면?

스프링 부트가 자동으로 QuestionRepository 객체를 자동으로 만들어 주입

객체를 주입하는 방식에는 @Autowired 외에
@Setter 메서드 또는 생성자를 사용하는 방식이 있음

순환 참조 문제와 같은 이유로 개발 시 @Autowired 보다는
생성자를 통한 객체 주입 방식을 권장

하지만 테스트 코드의 경우,

JUnit이 생성자를 통한 객체 주입을 지원하지 않으므로

테스트 코드 작성시에만 @Autowired 사용,

실제 코드 작성시에는 생성자를 통한 객체 주입 방식을 사용

순환 참조 문제란?
두 개 이상의 객체나 컴포넌트가 서로 참조하면서 무한히 돌고있는 상태
소프트웨어 설계나 개발 과정에서 자주 발생할 수 있는 문제 중 하나
메모리 누수, 무한 루프, 복잡성 증가로 인해 문제가 된다.

@Test 애너테이션을 붙이면 붙인 메서드가 테스트 메서드가 된다

@Test
	void testJpa() {
}

SbbApplicationTests 클래스를 JUnit으로 실행하면

@Test가 붙은 testJpa()가 실행된다

실행하게 되면 id가 자동으로 1씩 올라가는데

이거는 MySQL auto_increment << 이거랑 같다

@Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
CREATE TABLE STUDENT(
	STUDENT_ID INT UNSIGNED NOT NULL AUTO_INCREMENT,
);

데이터 조회 메서드

findAll 메서드

question 테이블에 저장된 모든 데이터를 조회하기 위해 리포지터리의 findAll 메서드를 사용

데이터 사이즈가 얼마인지 확인하고 싶다면,

JUnit의 assertEquals 메서드를 사용하기

이 메서드는 테스트에서 예상한 결과와 실제 결과가 같은지 확인하는 목적으로 사용한다.

JPA 또는 데이터베이스에서 올바르게 가져오는지 확인 용도

예시 )

@Test
void testJpa() {
	List<Question> all = this.questionRepository.findAll();
    assertEquals(2, all.size());
    
    Question q = all.get(0);
    assertEqauls("sbb가 무엇인가요?", q.getSubject());

assertEqauls(기댓값, 실젯값)
assertEqauls(2, all.size());


findById

id 값으로 데이터를 조회하기 위해서는 리포지터리의 findById 메서드를 사용해야 한다.

findById 의 리턴 타입은 Qustion이 아닌 Optional

Optional은 그 값을 처리하기 위한 (null 값을 유연하게 처리하기 위한) 클래스로

isPresent() 메서드로 값이 존재하는지 확인이 가능

만약 isPresent()로 값이 존재한다는 것을 확인했다면,
get() 메서드를 통해 실제 Question 객체의 값을 얻는다.

@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가 1인 질문을 검색,

이에 해당하는 질문의 제목이 "sbb가 무엇인가요?" 인 경우

JUnit 테스트를 통과한다.


findBySubject 메서드

리포지터리는 findBySubject 메서드를 기본적으로 제공하지 않음

그래서 QuestionRepository 인터페이스를 변경

조회 방법

public interface QuestionRepository extends JpaRepository<Question, Integer> {
    Question findBySubject(String subject);
}
@SpringBootTest
class PracticeApplicationTests {

	@Autowired
	private QuestionRepository questionRepository;

	@Test
	void testJpa() {
		Question q = this.questionRepository.findBySubject("sbb가 뭔고?");
		assertEquals(1, q.getId());
	}
}

이게 왜 될까?

JPA 가 리포지터리의 메서드명을 분석하여 쿼리를 만든 후
실행하는 기능이 있어서 가능한 것

findBy + 엔티티의 속성 이름 (findBySubject) 과 같은 리포지터리의 메서드를

작성하면 입력한 속성의 값으로 데이터를 조회 가능

쿼리문으로 보기

spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.show_sql=true

findBySubjectAndContent

함께 조회하기 위해서는 And 연산자를 사용

public interface QuestionRepository extends JpaRepository<Question, Integer> {
    Question findBySubject(String subject);
    Question findBySubjectAndContent(String subject, String content);
}
@SpringBootTest
class PracticeApplicationTests {

	@Autowired
	private QuestionRepository questionRepository;

	@Test
	void testJpa() {
		Question q = this.questionRepository.findBySubjectAndContent(
        "sbb가 뭔고?", "sbb 궁금하다");
		assertEquals(1, q.getId());
	}
}

메서드 조합 표

SQL 연산자리포지터리의 메서드 예시설명
AndfindBySubjectAndContent(String subject, String content)Subject, Content 열과 일치하는 데이터를 조회
OrfindBySubjectOrContent(String subject, String content)Subject 열 또는 Content 열과 일치하는 데이터를 조회
BetweenfindByCreateDateBetween(LocalDateTime fromDate, LocalDateTime toDate)CreateDate 열의 데이터 중 정해진 범위 내에 있는 데이터를 조회
LessThanfindByIdLessThan(Integer id)Id 열에서 조건보다 작은 데이터를 조회
GreaterThanEqualfindByIdGreaterThanEqual(Integer id)Id 열에서 조건보다 크거나 같은 데이터를 조회
LikefindBySubjectLike(String subject)Subject 열에서 문자열 ‘subject’와 같은 문자열을 포함한 데이터를 조회
InfindBySubjectIn(String[] subjects)Subject 열의 데이터가 주어진 배열에 포함되는 데이터만 조회
OrderByfindBySubjectOrderByCreateDateAsc(String subject)Subject 열 중 조건에 일치하는 데이터를 조회하여 CreateDate 열을 오름차순으로 정렬하여 반환

응답 결과가 여러 건인 경우

리턴 타입을 Question 이 아닌 List<Quesiton> 으로 해야한다.

findBySubjectLike

SQL에서는 특정 문자열을 포함한 데이터를 찾을 때 Like 를 사용

public interface QuestionRepository extends JpaRepository<Question, Integer> {
    Question findBySubject(String subject);
    Question findBySubjectAndContent(String subject, String content);
    List<Question> findBySubjectLike(String subject);
}
@SpringBootTest
class PracticeApplicationTests {

	@Autowired
	private QuestionRepository questionRepository;

	@Test
	void testJpa() {
		List<Question> qList = this.questionRepository.findBySubjectLike("sbb%");
		Question question = qList.get(0);
		assertEquals("sbb가 뭔고?", question.getSubject());
	}
}

Like 사용법

표기 예표기 위치에 따른 의미
sbb%'sbb'로 시작하는 문자열
%sbb'sbb'로 끝나는 문자열
%sbb%'sbb'를 포함하는 문자열

데이터 수정

@SpringBootTest
class PracticeApplicationTests {

	@Autowired
	private QuestionRepository questionRepository;

	@Test
	void testJpa() {
		Optional<Question> oq = this.questionRepository.findById(1);
		assertTrue(oq.isPresent());
		Question q = oq.get();
		q.setSubject("수정된 제목");
		this.questionRepository.save(q);
	}
}

assertTrue() 는 괄호 안의 값이 참 인지 테스트한다.
oq.isPresent() 가 false를 리턴하면 오류가 발생하고 테스트 종료

질문 엔티티를 조회 후, subject 속성을 '수정된 제목' 이라는 값으로 수정

변경된 질문을 데이터베이스에 저장하기 위해

this.questionRepository.save(q) 리포지터리의 save 메서드 사용

Optional : findById가 있을 수도 있고 없을 수도 있어서 리턴 타입으로 사용

그 값을 처리하기 위한 클래스 (null 값을 유연하게 처리하기 위한 클래스)

isPresent() 메서드로 값이 존재하는지 확인 가능

만약, isPresent() 를 통해 값이 존재한다는 것을 확인했다
그러면 get() 메서드를 통해 실제 Question 객체의 값을 얻는다.

데이터 삭제

리포지터리 delete 메서드로 데이터 삭제

원래 데이터는 2건이 있었는데, 삭제 후 1건만 남았는지에 대한 테스트 통과

@SpringBootTest
class PracticeApplicationTests {

	@Autowired
	private QuestionRepository questionRepository;

	@Test
	void testJpa() {
		assertEquals(2, this.questionRepository.count());
		Optional<Question> oq = this.questionRepository.findById(1);
		assertTrue(oq.isPresent());
		Question q = oq.get();
		this.questionRepository.delete(q);
		assertEquals(1, this.questionRepository.count());
	}
}

리포지터리의 count() 메서드는 테이블 행의 개수를 리턴

데이터 저장

@SpringBootTest
class SbbApplicationTests {

	@Autowired
	private QuestionRepository questionRepository;

	@Autowired
	private AnswerRepository answerRepository;

	@Test
	void testJpa() {
		Optional<Question> oq = this.questionRepository.findById(2);
		assertTrue(oq.isPresent());
		Question q = oq.get();

		Answer a = new Answer();
		a.setContent("네 자동으로 생성됩니다.");
		a.setQuestion(q);
		a.setCreateDate(LocalDateTime.now());
		this.answerRepository.save(a);
	}
}

질문 데이터를 저장할 때와 같이 리포지터리가 필요하므로

AnswerRepository 의 객체를 @Autowired 를 통해 주입

답변을 생성하려면 질문이 필요하므로 우선 질문을 조회

QuestionRepositoryfindById 메서드를 통해 id가 2인 질문 데이터를 가져와

답변의 question 속성에 대입해 답변 데이터를 생성

데이터 조회

@SpringBootTest
class SbbApplicationTests {

	@Autowired
	private QuestionRepository questionRepository;

	@Autowired
	private AnswerRepository answerRepository;

	@Test
	void testJpa() {
		Optional<Answer> oa = this.answerRepository.findById(1);
		assertTrue(oa.isPresent());
		Answer a = oa.get();
		assertEquals(2, a.getQuestion().getId());
	}
}

id 값이 1인 답변을 조회, 그리고 조회한 답변과 연결된 질문의 id가 2인지도 조회

답변 데이터로 질문 데이터 찾기 vs 질문 데이터로 답변 데이터 찾기

답변 엔티티의 question 속성을 이용하면 a.getQuestion 메서드를 사용해

(a는 답변 객체, a.getQuestion() 은 답변에 연결된 질문 객체)

'답변에 연결된 질문' 에 접근할 수 있다

답변에 연결된 질문 데이터를 찾는 건
Answer 엔티티에 question 속성이 이미 있어서 쉬움

반대의 경우는?
질문 엔티티에 정의한 answerList 를 사용하면 쉬움

@SpringBootTest
class SbbApplicationTests {

	@Autowired
	private QuestionRepository questionRepository;

	@Transactional
	@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());
	}
}
profile
코딩하는 곤쪽이

0개의 댓글