스프링 입문 주차
Updated:
Chapter 1
Gradle 이란 무엇일까?
Gradle은 빌드 자동화 시스템으로 Java 코드를 설정에 맞게 자동으로 Build해준다. build.gradle은 Gradle 기반의 빌드 스크립트로서 이 스크립트를 작성하면 소스 코드를 빌드하고 라이브러리들의 의존성을 쉽게 관리할 수 있다.
서버란 무엇일까?
네트워크는 여러 대의 컴퓨터 또는 장비가 연결되어서 정보를 주고 받을 수 있게 도와주는 기술로, 서로의 정보를 주고 받기 위해서는 IP 주소, 서브넷 마스크, 게이트웨이 등의 정보를 설정하고 네트워크 프로토콜을 이용하겨 통신하게 된다.
웹 서버는 인터넷을 통해 HTTP를 이용하여 웹상의 클라이언트의 요청을 응답해주는 통신을 하는 일종의 컴퓨터로, 브라우저를 통해 HTTP Request로 웹사이트를 웹 서버에 요청하고, 웹서버는 요청을 승인하고 HTTP Response를 통해 웹사이트 데이터를 브라우저에 전송한다. 이를 통해 브라우저는 서버에서 받아온 데이터를 이용해 웹사이트를 브라우저에 그려내는 일을 한다.
- API(Application Programming Interface)는 다른 소프트웨어 시스템과 통신하기 위해 따라야 하는 규칙을 정의한다.
- RESTful API: REST(Representational State Transfer)는 API 작동 방식에 대한 조건을 부과하는 소프트웨어 아키텍처이다. REST 아키텍처 스타일을 따르는 API를 REST API라고 한다. 즉, 서버의 api가 적절하게 http를 준수하며 잘 설계되어있으면 RESTful 하게 설계되어 있다고 생각할 수 있다.
- API가 적절하게 HTTP 메서드를 사용했다면, RESTful하게 설계했다고 볼 수 있다.
- HTTP(HyperText Transfer Protocol): 데이터를 주고 받는 양식을 정의한 통신 규약
- HTTP 상태코드를 통해 브라우저와 서버간의 요청, 응답 과정에서 발생할 수 있는 상황들을 표현할 수 있다.
Lombok과 application.properties
Lombok은 메서드, 생성자 등을 자동 생성해줌으로써 코드를 절약할 수 있도록 도와주는 라이브러리이다.
- @Getter, @Setter
- @AllArgsConstructor, NoArgsConstructor: 기본 생성자와 모든 필드를 가진 오버로딩된 생성자를 만들어 준다.
- @RequiredArgsConstructor: final 제어가가 붙은 필드를 파라미터로 가진 오버로딘된 생성자를 만들어 준다.
application.properties는 Spring과 관련된 설정을 할 때 사용되는 파일이다. 이 파일을 이용하면 자동으로 설정되고 있는 설정 값을 쉽게 수정할 수 있고 DB 연결 시 DB의 정보를 제공해야하는데 이러한 경우에도 이 파일을 이용하여 쉽게 값을 전달할 수 있다.
Spring MVC란 무엇일까?
MVC란 Model-View-Controller의 약자로, 소프트웨어 디자인 패턴 중 하나이다.

- Model: 데이터와 비즈니스 로직을 담당, 데이터베이스와 연동하여 데이터를 저장하고 불러오는 등의 작업을 수행
- View: 사용자 인터페이스를 담당, 사용자가 보는 화면과 버튼, 폼 등을 디자인하고 구현
- Controller: Model과 View 사이의 상호작용을 조정하고 제어, 사용자의 입력을 받아 Model에 전달하고, Model의 결과를 바탕으로 View를 업데이트
Spring MVC란 Spring에서 MVC 디자인 패턴을 적용하여 HTTP 요청을 효율적으로 처리하고 있다고 이해하면 된다.
Path Variable과 Request Param, Dto

- Path Variable: 브라우저에서 서버로 HTTP 요청을 보낼 때 데이터를 함께 보낼 수 있다.
- 서버에 보내려는 데이터를 URL 경로에 추가할 수 있다.
- Request Param: 서버에 보내려는 데이터를 URL 경로 마지막에 ?와 &를 사용하여 추가할 수 있다.
- @RequestBody: HTTP Body에 JSON 데이터를 담아 서버에 전달할 때 해당 Body 데이터를 Java의 객체로 전달 받을 수 있다.
- 예를 들어 HTTP Body에
{"name": "Robbie", "age": "95}의 JSON 형태로 데이터가 서버에 전달되었을 때 @RequestBody 애너테이션을 사용해 데이터를 객체 형태로 받을 수 있다.@PostMapping("/form/json") public String helloPostRequestJson(@RequestBody Star star) { return ...; }
- 예를 들어 HTTP Body에
- DTO(Data Transfer Object): 데이터 전송 및 이동을 위해 생성되는 객체
- Request의 데이터를 처리할 때는 사용되는 객체는 RequestDto
- Response를 할 때 사용되는 객체는 ResponseDto
Database, SQL, JDBC
- DBMS(Database Management System): Database를 관리하고 운영하는 소프트웨어
- RDBMS: Relational DBMS의 약자로 관계형 데이터베이스라고 불림
SQL(Structured Query Language)은 RDBMS에서 사용되는 언어이다.
- DDL(Data Definition Language): CREATE, ALTER, DROP, TRUNCATE
- DCL(Data Control Language): GRANT, REVOKE
- DML(Data Manipulation Language): INSERT, SELECT, UDATE, DELETE
JDBC(Java Database Connectivity)로 DB에 접근할 수 있도록 Java에서 제공하는 API이다. JDBC에 연결해야하는 DB에 JDBC 인터페이스를 구현한 후 라이브러리를 제공하는데 이를 JDBC 드라이버라 부른다. 따라서 MySQL 드라이버를 사용해 DB를 연결하다가 PostgrSQL 서버로 변경이 필요할 때 드라이버만 교체하면 손쉽게 DB 변경이 가능한다.
Chapter 2
3 Layer Architecture

- Controller: 클라이언트의 요청을 받음, 요청에 대한 로직 처리는 Service에게 전담, Service에서 처리 완료된 결과를 클라이언트에게 응답
- Service: 사용자의 요구사항을 처리, DB 저장 및 조회가 필요할 때는 Repository에게 요청
- Repository: DB 관리
IoC와 DI
- IoC(Inversion of Control): 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것
- DI(Dependency Injection): 객체가 자신의 의존성을 외부로부터 주입받도록 설계된 패턴
- Bean: Spring이 관리하는 객체
- Spring IoC 컨테이너: Bean을 모아둔 컨테이너
Spring Bean을 등록하는 방법은 다음과 같다.
- @Component
- Bean으로 등록하고자하는 클래스 위에 설정
- @ComponentScan
- Spring 서버가 실행 될 때 @ComponentScan에 설정해 준 packages위치와 하위 packages 들을 전부 확인하여 @Component가 설정된 클래스들을 Bean으로 등록해 줌
Spring Bean 사용 하려면 필드 위에 @Autowired를 작성해야 하는데 Spring 4.3 부터는 @Autowired가 생략이 가능하다(생성자 선언이 1개일 때만 가능). 하지만 @RequiredArgsConstructor를 사용하면 생성자를 생략해도 된다.
Spring 3 Layer Annotation은 Controller, Service, Repository의 역할로 구성된 클래스들을 Bean으로 등록할 때 해당 Bean 클래스의 역할을 명시하기 위해 사용된다.
- @Controller, @RestController
- @Service
- @Repository
JPA CORE
ORM(Object-Relational Mapping)이란 객체 지향 언어(Object)와 관계형 데이터베이스(Relational)의 테이블을 자동으로 연결(Mapping)해 주는 기술이고, JPA(Java Persistence API)는 자바 ORM 기술에 대한 표준 명세이다.
- 하이버네이트(Hibernate): JPA는 표준 명세이고, 이를 실제 구현한 프레임워크 중 표준이 hibernate이다.
Entity는 JPA에서 관리되는 클래스 즉, 객체를 의미한다. Entity 클래스는 DB의 테이블과 매핑되어 JPA에 의해 관리된다.
@Entity
@Table(name = "memo")
public class Memo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username", nullable = false, unique = true)
private String username;
@Column(name = "contents", nullable = false, length = 500)
private String contents;
}
- @Entity: JPA가 관리할 수 있는 Entity 클래스로 지정할 수 있음
- @Entity(name=”Memo”): Entity 클래스 이름을 지정할 수 있음(default: 클래스명)
- @Table: 매핑할 테이블을 지정
- @Table(name = “memo”): 매핑할 테이블의 이름을 지정(default: Entity 명)
- @Column
- name = “username”: 필드와 매핑할 테이블의 column을 지정(default: 객체의 필드명)
- nullable = false: 데이터의 null 값 허용 여부를 지정(default: true)
- unique = true: 데이터의 중복 값 허용 여부를 지정(default: false)
- length = 500: 데이터 값의 길이 제약조건(default: 255)
- @Id: 테이블의 기본 키를 지정
- @GeneratedValue(strategy = GenerationType.IDENTITY) 옵션을 추가하면 키 생성을 DB에 위임할 수 있음
영속성 컨텍스트는 Entity 객체를 효율적으로 쉽게 관리하기 위해 만들어진 공간이다. 영속성 컨텍스트에 접근하여 Entity 객체들을 조작하기 위해서는 EntityManager가 필요하고 EntityManager는 EntityManagerFactory를 통해 생성하여 사용할 수 있다. EntityManagerFactory를 만들기 위해서는 DB에 대한 정보를 전달해야한다(ex. persisstence.xml).
트랜잭션은 DB에 데이터들의 무결성과 정합성을 유지하기 위한 하나의 논리적인 개념으로, 모든 SQL이 성공적으로 수행되면 DB에 영구적으로 변경을 반영하지만, SQL 중 단 하나라고 실패한다면 모든 변경을 되돌린다. DB에서 하나의 트랜잭션에 여러 SQL을 포함하고 있다가 마지막에 영구적으로 변경을 반영한 것처럼 JPA에서도 영속성 컨텍스트로 관리하고 있는 변경이 발생한 객체들의 정보를 쓰기 지연 저장소에 전부 가지고 있다가 마지막에 SQL을 DB에 요청해 변경을 반영한다.
@Test
@DisplayName("EntityTransaction 성공 테스트")
void test1() {
EntityTransaction et = em.getTransaction(); // EntityManager 에서 EntityTransaction 을 가져옵니다.
et.begin(); // 트랜잭션을 시작합니다.
try { // DB 작업을 수행합니다.
Memo memo = new Memo(); // 저장할 Entity 객체를 생성합니다.
memo.setId(1L); // 식별자 값을 넣어줍니다.
memo.setUsername("Robbie");
memo.setContents("영속성 컨텍스트와 트랜잭션 이해하기");
em.persist(memo); // EntityManager 사용하여 memo 객체를 영속성 컨텍스트에 저장합니다.
et.commit(); // 오류가 발생하지 않고 정상적으로 수행되었다면 commit을 호출합니다.
// commit 이 호출되면서 DB 에 수행한 DB 작업들이 반영됩니다.
} catch (Exception ex) {
ex.printStackTrace();
et.rollback(); // DB 작업 중 오류 발생 시 rollback 을 호출합니다.
} finally {
em.close(); // 사용한 EntityManager 를 종료합니다.
}
emf.close(); // 사용한 EntityManagerFactory 를 종료합니다.
}
영속성 컨텍스트가 어떻게 Entity 객체를 효율적으로 관리하고 있는지 살펴보자.

영속성 컨텍스트는 내부적으로 cache 저장소를 가지고 있다. 이 저장소는 Map 자료구조 형태로 key에는 @Id로 매핑한 기본 키, value에는 해당 Entity 클래스의 객체를 저장한다. 이는 em.persist(memo);와 같이 메서드가 호출되면 memo Entity 객체를 캐시 저장소에 저장한다.

이렇게 저장된 Entity는 em.find(Memo.class, 1);과 같이 조회할 수 있다.
- 캐시 저장소에 조회하는 Id가 존재하지 않은 경우
- DB에 SELECT 조회 후 캐시 저장소에 저장하고 반환
- 캐시 저장소에 조회하면 Id가 존재하는 경우
- 캐시 저장소에 조회하는 Id가 존재하면 해당 Entity 객체를 반환
이런 1차 cache는 DB 조회 횟수를 줄여주고 DB row 1개 당 객체 1개가 사용되는 것을 보장한다(객체 동일성 보장).
Entity는 삭제할 Entity를 조회한 후 캐시 저장소에 없다면 DB에 조회해 저장하고 em.remove(entity);와 같이 삭제할 Entity를 DELETED 상태로 만든 후 트랜잭션 commit 후 Delete SQL이 DB에 요청된다.
JPA는 트랜잭션 처럼 SQL을 모아서 한번에 DB에 반영한다. JPA는 이를 구현하기 위해 쓰기 지연 저장소를 만들어 SQL을 모아두고 있다가 트랜잭션 commit 후 한번에 DB에 반영한다.
commit 후 추가적인 동작으로 em.flush(); 메서드가 있다. flush 메서드는 영속성 컨텍스트의 변경 내용들을 DB에 반영하는 역할을 수행한다. 참고로 트랜잭션을 설정하지 않고 flush 메서드를 호출하면 no transaction is in progress 메시지와 함께 TransactionRequiredException 오류가 발생한다.
@Test
@DisplayName("flush() 메서드 확인")
void test7() {
EntityTransaction et = em.getTransaction();
et.begin();
try {
Memo memo = new Memo();
memo.setId(4L);
memo.setUsername("Flush");
memo.setContents("Flush() 메서드 호출");
em.persist(memo);
System.out.println("flush() 전");
em.flush(); // flush() 직접 호출
System.out.println("flush() 후\n");
System.out.println("트랜잭션 commit 전");
et.commit();
System.out.println("트랜잭션 commit 후");
} catch (Exception ex) {
ex.printStackTrace();
et.rollback();
} finally {
em.close();
}
emf.close();
}

JPA는 em.update(entity);와 같이 update 메서드를 지원하지 않는다. 이는 트랜잭션이 commit 되고 em.flush();가 호출되면 Entity의 현재 상태와 최초 상태를 비교하고 변경 내용이 있다면 Update SQL을 생성하여 쓰기 지연 저장소에 저장하고 모든 쓰기 지연 저장소의 SQL을 DB에 요청하고 DB의 트랜잭션이 commit되면서 반영된다. 즉, 변경하고 싶은 데이터가 있다면 먼저 데이터를 조회하고 해당 Entity객체의 데이터를 변경하면 자동으로 Update SQL이 생성되고 DB에 반영된다. 이러한 과정을 Dirty Checking(변경 감지)라고 한다.
Entity는 영속성 컨텍스트에 저장되지 않으면 JPA의 관리를 받지 않는다. 즉, 비영속 Entity를 EntityManager를 통해 영속성 컨텍스트에 저장하여 관리되고 있는 상태로 만들어야 한다.
Memo memo = new Memo(); // 비영속 상태
memo.setId(1L);
memo.setUsername("Robbie");
memo.setContents("비영속과 영속 상태");
em.persist(memo); // 영속
준영속(Detached) 상태는 영속성 컨텍스트에 저장되어 관리되다가 분리된 상태를 의미한다. em.detached(entity);와 같은 메서드로 특정 Entity만 준영속 상태로 전환할 수 있다.
@Test
@DisplayName("준영속 상태 : detach()")
void test2() {
EntityTransaction et = em.getTransaction();
et.begin();
try {
Memo memo = em.find(Memo.class, 1);
System.out.println("memo.getId() = " + memo.getId());
System.out.println("memo.getUsername() = " + memo.getUsername());
System.out.println("memo.getContents() = " + memo.getContents());
// em.contains(entity) : Entity 객체가 현재 영속성 컨텍스트에 저장되어 관리되는 상태인지 확인하는 메서드
System.out.println("em.contains(memo) = " + em.contains(memo));
System.out.println("detach() 호출");
em.detach(memo); // 특정 Entity 객체를 영속성 컨텍스트에서 제거
System.out.println("em.contains(memo) = " + em.contains(memo));
System.out.println("memo Entity 객체 수정 시도");
memo.setUsername("Update");
memo.setContents("memo Entity Update");
System.out.println("트랜잭션 commit 전");
et.commit();
System.out.println("트랜잭션 commit 후");
} catch (Exception ex) {
ex.printStackTrace();
et.rollback();
} finally {
em.close();
}
emf.close();
}
영속성 컨텍스트는 em.clear();와 같은 메서드로 완전히 초기화 할 수 있다. clear() 메서드는 영속성 컨텍스트의 모든 Entity를 준영속 상태로 전환한다. 영속성 컨텍스트 틀은 유지하지만 내용은 비워 새로 만든 것과 같은 상태가 된다.
@Test
@DisplayName("준영속 상태 : clear()")
void test3() {
EntityTransaction et = em.getTransaction();
et.begin();
try {
Memo memo1 = em.find(Memo.class, 1);
Memo memo2 = em.find(Memo.class, 2);
// em.contains(entity) : Entity 객체가 현재 영속성 컨텍스트에 저장되어 관리되는 상태인지 확인하는 메서드
System.out.println("em.contains(memo1) = " + em.contains(memo1));
System.out.println("em.contains(memo2) = " + em.contains(memo2));
System.out.println("clear() 호출");
em.clear();
System.out.println("em.contains(memo1) = " + em.contains(memo1));
System.out.println("em.contains(memo2) = " + em.contains(memo2));
System.out.println("memo#1 Entity 다시 조회");
Memo memo = em.find(Memo.class, 1);
System.out.println("em.contains(memo) = " + em.contains(memo));
System.out.println("\n memo Entity 수정 시도");
memo.setUsername("Update");
memo.setContents("memo Entity Update");
System.out.println("트랜잭션 commit 전");
et.commit();
System.out.println("트랜잭션 commit 후");
} catch (Exception ex) {
ex.printStackTrace();
et.rollback();
} finally {
em.close();
}
emf.close();
}
또한 영속성 컨텍스트는 em.close();와 같은 메서드로 영속성 컨텍스트를 종료할 수 있다. close() 메서드는 해당 영속성 컨택스트가 관리하던 영속성 상태의 Entity들을 모두 준영속 상태로 변경한다.
@Test
@DisplayName("준영속 상태 : close()")
void test4() {
EntityTransaction et = em.getTransaction();
et.begin();
try {
Memo memo1 = em.find(Memo.class, 1);
Memo memo2 = em.find(Memo.class, 2);
// em.contains(entity) : Entity 객체가 현재 영속성 컨텍스트에 저장되어 관리되는 상태인지 확인하는 메서드
System.out.println("em.contains(memo1) = " + em.contains(memo1));
System.out.println("em.contains(memo2) = " + em.contains(memo2));
System.out.println("close() 호출");
em.close();
Memo memo = em.find(Memo.class, 2); // Session/EntityManager is closed 메시지와 함께 오류 발생
System.out.println("memo.getId() = " + memo.getId());
} catch (Exception ex) {
ex.printStackTrace();
et.rollback();
} finally {
em.close();
}
emf.close();
}
이렇게 준영속 상태로 변경된 Entity들을 다시 영속 상태로 바꾸기 위해 merge(memo); 와 같은 메서드를 사용한다. merge(entity) 메서드는 전달받은 Entity를 사용해서 영속 상태의 Entity를 반환한다. merge(entity) 메서드는 다음과 같이 동작한다.
- 해당 Entity가 영속성 컨택스트에 없다면 DB에서 새롭게 조회한 후 이 Entity를 영속성 컨텍스트에 저장하고 전달 받은 Entity의 값을 사용해 병합한다. 이후 Update SQL이 수행된다.(수정)
- 만약 DB에서도 없다면 새롭게 생성한 Entity를 영속성 컨텍스트에 저장하고 Insert SQL이 수행된다.(저장)
위와 같이 merge(entity) 메서드는 비영속, 준영속 모두 파라미터로 받을 수 있으며 상황에 따라 저장할 수도 수정할 수도 있다. 마지막으로 em.remove(memo)와 같이 삭제하기 위해 조회해온 영속 상태의 Entity를 파라미터로 전달받아 삭제 상태로 전환할 수 있다.
Spring Data JPA
SpringBoot 환경에서 JPA를 설정해보자. JPA Dependency를 추가하고 application.properties에서 Hibernate를 설정한다.
- show_sql, format_sql, use_sql_comments: Hibernate가 DB에 요청하는 모든 SQL을 출력
- ddl-auto
- create: 기존 테이블 삭제 후 다시 생성(DROP + CREATE)
- create-drop: create와 같으나 종료시점에 테이블을 DROP
- update: 변경된 부분만 반영
- validate: Entity와 테이블이 정상 매팽되었는지만 확인
- none: 아무것도 하지 않음
SpringBoot 환경에서는 EntityManagerFactory와 EntityManager를 자동으로 생성해준다.
Spring 프레임워크에서는 @Transactional 애너테이션을 클래스나 메서드에 추가하면 트랜잭션을 적용할 수 있다. 메서드가 호출되면, 해당 메서드 내에서 수행되는 모든 DB 연산 내용은 하나의 트랜잭션으로 묶인다. 이때, 해당 메서드가 정상적으로 수행되면 트랜잭션을 커밋하고, 예외가 발생하면 롤백한다. 이때, save 메서드는 readOnly = true 옵션인 @Transactional을 덮어쓰게 되어 readOnly=false 옵션으로 적용된다. readOnly=true 옵션은 트랜잭션에서 데이터를 읽기만 할 때 사용되고 읽기 작업에 대한 최적화를 수행할 수 있다.
@Transactional에서 트랜잭션 전파 옵션을 지정할 수 있다(default: REQUIRED). REQUIRED 옵션은 부모 메서드에 트랜잭션이 존재하면 자식 메서드의 트랜잭션은 부모의 트랜잭션에 합류하게 된다.
Spring Data JPA는 JPA를 쉽게 사용할 수 있게 만들어 놓은 하나의 모듈이다. Repository 인터페이스는 Hibernate와 같은 JPA 구현체를 사용해서 구현한 클래스를 통해 사용된다. Spring Data JPA를 사용하려면 JpaRepository<”@Entity 클래스”,”@Id의 데이터 타입”>을 상속받는 interface로 선언한다. 이러면 Spring Data JPA에 의해 자동으로 Bean으로 등록이 된다.
- save: 데이터 저장
- findAll: 해당 테이블의 전체 데이터를 조회
- findById:
- JpaRepository에 update 메서드는 존재하지 않아 다음과 같이 update를 구현
@Transactional public Long updateMemo(Long id, MemoRequestDto requestDto) { Memo memo = findMemo(id); memo.update(requestDto); // Memo Class에 update 메서드 구현 return id; } - delete: 해당 데이터를 테이블에서 삭제
데이터의 생성(created_at), 수정(modified_at) 시간은 매우 자주 활용된다. 각각의 Entity의 생성 수정 시간을 매번 작성하는 것은 너무 비효율적이다. Sprig Data JPA에서는 시간에 대해서 자동으로 값을 넣어주는 기능인 JPA Audition을 제공하고 있다.
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Timestamped {
@CreatedDate
@Column(updatable = false)
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime createdAt;
@LastModifiedDate
@Column
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime modifiedAt;
}
- @MappedSuperclass: Jpa Entity 클래스들이 해당 추상 클래스를 상속할 경우 createdAt, modifiedAt 처럼 추상 클래스에 선언한 맴버변수를 컬럼으로 인식할 수 있음
- @EntityListeners(AuditingEntityListener.class): 해당 클래스에 Auditing 기능을 포함
- @CreatedDate: Entity 객체가 생성되어 저장될 때 시간이 자동 저장. 수정하면 안되기 때문에 updatable = false 옵션 추가
- @LastModifiedDate: 조회한 Entity 객체의 값을 변경할 때 변경된 시간이 자동 저장
- @Temporal: 날짜 타입을 매팅할 때 사용(Date, Time, TimeStamp)
JPA Auditing 기능을 사용하겠다는 정보를 전달해주기 위해 @SpringBootApplication이 있는 class에 @EnableJpaAuditing을 추가해야 한다.
Spring Date JPA에서는 메서드 이름으로 SQL을 생성할 수 있는 Query Methods 기능을 제공한다. JpaRepository 인터페이스에서 해당 인터페이스와 매핑되어 있는 테이블에 요청하고자 하는 SQL을 메서드 이름을 사용하여 선언할 수 있다.
- findAllByOrderByModifiedAtDesc: 수정 시간을 기준으로 전체 데이터를 내림차순으로 가져오는 SQL을 실행하는 메서드 생성
- List
findAllByUsername(String username): ByUsername에 값을 전달해줘야하기 때문에 파라미너에 해당 값의 타입과 변수명을 선언해 줘야 함
댓글남기기