스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술을 들으며 정리하는 POST입니다.
전체적인 흐름
- Spring Project 생성
- Spring boot로 웹 서버 실행
- 회원 도메인 개발
- 웹 MVC 개발
- DB 연동 - JDBC, JPA, Spring data JPA
- 테스트 케이스 작성
메모리 기반의 애플리케이션은 실무에서는 사용될 수 없다.
수 많은 데이터를 기록하고 영구적으로 저장하기 위해서는 Database를 사용하여야 한다.
여기서 사용할 Database는 아주 가볍고 간단한 "H2 Database" 를 사용할 것이다.
H2 Database는 다음과 같은 특징을 가진다.
Terminal
을 열어 h2/bin
으로 이동한다.chmod 755 h2.sh
명령어로 h2.sh
에 대한 권한을 부여한다. (MAC 유저의 경우)./h2.sh
로 실행연결
을 누른다.Terminal
을 켜서, home
directory에서 ls -al
로 test.mv.db
가 존재하는지 확인한다.test.mv.db
의 존재를 확인한 이후, 다음의 접근부터는 이처럼 파일로 접근하는 것이 아니라 JDBC URL:
에 jdbc:h2:tcp://localhost/~/test
을 입력하여 socket으로 접근하도록 한다. 파일로 접근하게 되면 application과 web console이 동시에 접근했을 때 충돌이 발생할 수 있기 때문이다.
rm test.mv.db
로 파일을 지우고, h2.sh
을 재시작하여 처음부터 수행한다.구현했던 Member
domain과 동일하게 table을 생성한다.
create table member (
id bigint generated by default as identity,
name varchar(255),
pwd integer,
phone varchar(11),
primary key (id)
);
위와 같이 table이 생성된 것을 확인할 수 있다.
이를 이전에 생성했던 domain/Member.java
의 타입과 비교해보면,
private Long id; == (h2)bigint
private String name; == (h2)varchar
private int pwd; == (h2)integer
private String phone; == (h2)varchar
id bigint generated by default as identity
인데, 이는 값이 설정되지 않은 채로 INSERT 되면, H2 DB가 자동으로 해당 field의 값을 할당해준다.이제 DB에 record를 하나 insert해보자.
insert into member(name) values('spring');
insert into member(name, pwd, phone) values('spring2', 12345, '01012341234');
결과는 다음과 같다.
NOT NULL
특성을 부여하지 않았기에 null
값으로 채워진다.id
가 순차적으로 증가하는 것을 확인할 수 있다.Database file에 대한 관리를 하기 위해,
project folder에 sql
directory를 하나 생성한다.
그리고 sql/ddl.sql
파일을 생성해 아래와 같이 sql 문들을 저장하여 관리한다.
구현한 application에서 H2 Database에 접근하는 방법에 대해 알아보도록 한다.
build.gradle
파일에 jdbc, h2 db 관련 라이브러리를 추가한다.
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
}
추가적으로 DB에 접속하려면, 접속 정보가 필요하다.
예전에는 개발자가 일일이 정보를 입력해야 했지만, Spring 은 경로 정보만을 이용하여 이를 처리해준다.
src/main/resources/application.properties
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
h2.Driver
에 에러가 뜰텐데, 아까 수정한 build.gradle
의 변경사항이 적용되지 않아 그런 것이므로, Load Gradle Changes
를 해주면 된다.spring.datasource.username=sa
를 명시하지 않으면 org.h2.jdbc.JdbcSQLInvalidAuthorizationSpecException: Wrong user name or password [28000-200]
이라는 에러가 발생한다.현재, 회원을 저장, 조회하는 역할은 MemberRepository
에서 수행하지만,
구현은 Memory 에 하는 방식이기에 MemoryMemberRepository
를 사용했었다.
하지만 이제 H2 DB와 연동하여 구현하기 위해서 새로운 Class를 생성한다.
repository/JdbcMemberRepository.java
먼저, DB와 연동하여 사용하기 위해서 DataSource
라는 것이 필요하다.
그리고, 이를 spring으로부터 주입받아야 한다.
DataSource
를 생성한다. 그리고 이를 주입받는 것이다.import javax.sql.DataSource;
...
private final DataSource dataSource;
public JdbcMemberRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
private Connection getConnection() {
return DataSourceUtils.getConnection(dataSource);
}
getConnection
을 수행한다.Connection
이 생성되는 것을 방지하여 트랜잭션에 대한 처리를 수행하기 위해서이다.@Override
public Member save(Member member) {
String sql = "insert into member(name, pwd, phone) values(?, ?, ?)";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
pstmt.setString(1, member.getName());
pstmt.setInt(2, member.getPwd());
pstmt.setString(3, member.getPhone());
pstmt.executeUpdate();
rs = pstmt.getGeneratedKeys();
if (rs.next()) {
member.setId(rs.getLong(1));
} else {
throw new SQLException("id 조회 실패");
}
return member;
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
String sql
은 사용할 query의 format을 담고 있다.PreparedStatement
를 통해, 인자를 query문에 담아 DB에 전달한다.ResultSet
은 결과를 받아오는 객체이다.Statement.RETURN_GENERATED_KEYS
: DB에 insert할 때, id
에 key값을 할당해주기 위해 필요한 옵션pstmt.setString(1, member.getName());
로 query에 인자를 추가해주고, pstmt.executeUpdate();
로 query를 수행한다.rs = pstmt.getGeneratedKeys();
는 위에서 설정한 옵션과 함께 사용되어야 한다. 이는 생성한 Key
를 반환한다.rs.next()
로 ResultSet
에 있는 값을 꺼낼 수 있다.try - catch
구문을 사용하여 exception
에 대한 처리를 해주고, 마지막에는 사용한 자원들 (Connection
, PreparedStatement
, ResultSet
) 에 대한 release 를 해야 한다.가만히 생각해보니,
findByPwd()
라는 method가 필요할까? 라는 생각이 들었다.
비밀번호를 가지고 회원을 조회하는 일은 본 적이 없는 것 같아 폐기처분한다.
조회 기능은 매우 비슷하기 때문에 findById()
만을 설명한다.
@Override
public Optional<Member> findById(Long id) {
String sql = "select * from member where id = ?";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setLong(1, id);
rs = pstmt.executeQuery();
if(rs.next()) {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
member.setPwd(rs.getInt("pwd"));
member.setPhone(rs.getString("phone"));
return Optional.of(member);
} else {
return Optional.empty();
}
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
Connection
을 수행하고, query를 날린다.save()
와는 다르게 SELECT query이므로 executeQuery()
를 사용한다.Member
객체를 생성하여 이를 반환해준다.findAll()
은 통째로 조회하는 것이므로 조건이 있는 findBy~()
보다 단순한 구조이다.
@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"));
member.setPwd(rs.getInt("pwd"));
member.setPhone(rs.getString("phone"));
members.add(member);
}
return members;
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
List<Member>
형태로 반환받기 때문에, while()
을 이용하여 결과 List를 반환한다.사용한 자원에 대한 release는 역순 으로 진행한다.
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) {
close(conn);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
private void close(Connection conn) throws SQLException {
DataSourceUtils.releaseConnection(conn, dataSource);
}
이전에 MemoryMemberRepository
로 구현했을 때, SpringConfig
file에서 스프링 컨테이너에 MemoryMemberRepository
를 등록하는 작업을 거쳤다.
이를 방금 생성한 JdbcMemberRepository
를 등록하는 것으로 변경한다.
그리고 JdbcMemberRepository
는 DataSource dataSource
를 필요로 한다.
이는 Spring에서 제공해주는데, 다음과 같은 방법이 있다.
SpringConfig
Autowired DataSource dataSource;
or
private DataSource dataSource;
@Autowired
public SpringConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
DataSource dataSource
에 대한 Bean을 생성해 의존성 주입을 수행해준다.SpringConfig
만을 변경시킴으로써, 수정된 코드에 대한 의존성을 수정할 수 있다.
./h2.sh
로 H2 DB를 동작시키고, 스프링을 동작시켜 회원 등록 및 목록 조회를 하면, DB와 연동하여 정상적으로 등록, 조회가 이루어지는 것을 확인할 수 있다.
Spring을 사용하는 이유
- 다형성의 활용: 인터페이스를 두고, 구현체를 바꿔서 사용할 수 있다. 이는 스프링 컨테이너가 DI를 이용해 지원한다. (memoryRepository → JdbcRepository)
아래는 구현 클래스에 대한 설명을 그림으로 표현한 것이다.
아래는 스프링 컨테이너에서 구현 클래스와의 연결을 표현한 그림이다.
SpringConfig
)만으로 구현 클래스를 변경할 수 있다.이전에 작성한 MemoryMemberRepositoryTest
의 코드들은 스프링과는 관련없이 순수하게 자바 코드만으로 테스트를 수행한 것이다.
test/.../MemberServiceTest.java
순수한 자바 코드였기에, JVM 안에서 실행되어 빠르게 실행된다.
이제는 DB까지 연결한 스프링 통합 테스트를 수행해보도록 한다.
우선 이전의 MemberServiceTest.java
를 복사하여MemberServiceIntegrationTest.java
를 생성한다.
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
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
@Transactional
class MemberServiceIntegrationTest {
@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
}
}
@BeforeEach
annotation을 사용하는 method를 삭제한다.@Autowired
를 사용)MemoryMemberRepository
가 아니라 MemberRepository
를 불러온다.SpringConfig
를 통해 구현체가 올라왔기 때문이다?@AfterEach
method를 삭제한다.@Transactional
의 존재로 필요가 없어졌다.@Transactional
을 주석 처리하고 회원가입 Test를 수행하면, Test가 끝난 이후에도, DB에 테스트 가입한 회원의 정보가 그대로 남아있다.select, insert
등의 query를 날린 후, rollback
하여 DB 수정사항을 되돌리는 로직이 필요하다.@Transactional
annotationd을 Test case에 달면, Test 실행 전, Transaction 을 실행한 후, Test가 끝날 때 rollback 을 수행한다. 따라서, DB는 반복 수행에도 문제가 없는 상태가 된다.Test를 실행하면, console을 확인하여 Spring 이 함께 동작하는 것을 확인할 수 있다. (@SpringBootTest
)
SpringConfig
에 작성한 내용도 함께 올라오는 것을 확인할 수 있다.Test를 실행할 때, 이미 DB에 저장되어 있는 정보와 겹쳐 에러가 발생하지 않도록 유의한다.
그럼 이전에 구현한
MemberServiceTest
는 필요가 없는가?
MemberServiceTest
는 "단위 테스트",
MemberServiceIntegrationTest
는 "통합 테스트" 라고 한다.
- 가급적으로, 단위 테스트가 훨씬 좋은 테스트일 확률이 높다. 컨테이너까지 올려야 하는 테스트인 경우 테스트 설계가 잘못되었을 확률이 높다.
JDBC API에서의 반복적인 코드를 제거해준다. 하지만 SQL query는 직접 작성해야 한다.
repository/JdbcTemplateMemberRepository.java
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
import java.util.List;
import java.util.Optional;
public class JdbcTemplateMemberRepository implements MemberRepository {
private final JdbcTemplate jdbcTemplate;
// @Autowired 생략 가능
public JdbcTemplateMemberRepository(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
...
}
JdbcTemplate
이 존재하므로, 이를 사용한다. 이는 Injection을 받을 수 있는 것이 아니라 DataSource
가 필요하다.class의 생성자가 딱 1개인 경우, Spring bean으로 등록 시,
@Autowired
를 생략할 수 있다.
RowMapper
methodprivate RowMapper<Member> memberRowMapper() {
return (rs, rowNum) -> {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
member.setPwd(rs.getInt("pwd"));
member.setPhone(rs.getString("phone"));
return member;
};
}
RowMapper
라는 것이 필요하다.RowMapper
는 query의 결과를 객체로 받아온다. 이전에는 ResultSet
에 결과를 받고, 이를 객체로 생성해서 반환하는 과정을 거쳤지만 RowMapper
는 내부적으로 ResultSet
을 사용하여 위 과정을 수행한다.@Override
public Optional<Member> findById(Long id) {
List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(), id);
return result.stream().findAny();
}
id
값을 ?
자리에 치환하고, 결과를 memberRowMapper()
를 통해 받아온다.List<Member>
이므로 result
에 받고, 결과를 Optional<>
로 변환하여 반환한다.findAll
)@Override
public List<Member> findAll() {
return jdbcTemplate.query("select * from member", memberRowMapper());
}
@Override
public Member save(Member member) {
SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");
Map<String, Object> parameters = new HashMap<>();
parameters.put("name", member.getName());
parameters.put("pwd", member.getPwd());
parameters.put("phone", member.getPhone());
Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
member.setId(key.longValue());
return member;
}
SimpleJdbcInsert
를 사용하여 query를 짤 필요 없이 구현이 가능하다.insert
는 구현 가능하기에 사용한다.executeAndReturnKey
를 이용해 생성한 회원의 key
를 받고 이를 저장한 Member
객체를 반환한다.SpringConfig
이제 구현한 DB 접근 class를 연결하기 위한 설정을 한다.
@Bean
public MemberRepository memberRepository() {
//return new JdbcMemberRepository(dataSource);
return new JdbcTemplateMemberRepository(dataSource);
}
이제는 스프링 통합 테스트를 구현했기 때문에 Web Application을 실행할 필요 없이 DB만 접속하고 통합 테스트만 수행하면 된다.
JdbcTemplate 을 사용하여 반복적인 코드를 많이 줄인 것은 맞지만, 여전히 SQL query는 개발자가 직접 작성해야 한다.
이번 장에서는 Interface의 일종인 JPA 를 사용하여 query 또한 자동으로 처리하도록 한다. 이를 통해
build.gradle
dependencies {
...
// implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
application.properties
JPA와 관련된 설정을 추가한다.
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
show-sql=true
: JPA가 생성하는 sql을 볼 수 있다.ddl-auto=none
: JPA는 객체를 확인하고 이에 대한 Table을 생성한다. 하지만 이미 Table을 생성했고, 생성한 Table을 사용할 것이므로, 해당 기능을 none
설정한다.JPA는 자바 표준의 interface이다. 그래서 구현은 여러 vendor들이 수행한다. 여기서는
hibernate
library를 중점적으로 사용하여 구현한다.
JPA는 객체와 ORM이라는 기술을 사용한다. Object Relational Mapping, 여기서 Mapping은 annotation을 사용하여 수행한다.
JPA를 사용하기 위해서는 Entity Mapping 이 필요하다.
domain/Member.java
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
...
@Entity
: JPA가 관리할 Entity임을 명시한다.@Id
: primary key로 지정한다.@GeneratedValue(strategy = GenerationType.IDENTITY)
: DB에서 고유하게 값을 자동으로 생성한다. 사용자가 insert하지 않는다.@Column(name = "원하는 column명")
private String name;
이제 repository를 생성한다.
repository/JpaMemberRepository.java
public class JpaMemberRepository implements MemberRepository {
private final EntityManager em;
public JpaMemberRepository(EntityManager em) {
this.em = em;
}
...
EntityManager
: JPA는 EntityManager
를 통해 모든 것이 동작한다. 위에서 추가한 data-jpa
dependency를 통해 JPA가 자동으로 현재 DB와 연결된 EntityManager
를 생성한다. 따라서 이를 Injection 받아 사용하면 된다.@Override
public Member save(Member member) {
em.persist(member);
return member;
}
em.persist(member)
: 객체를 저장하기 위한 query를 내부적으로 작성한다.@Override
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id);
return Optional.ofNullable(member);
}
em.find(Member.class, id)
: 조회할 Type과 식별자를 전달하여 조회한다.Optional
이므로 이에 맞게 반환한다.JPQL이라는 객체 지향 쿼리를 사용한다.
@Override
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
Member
)를 대상으로 query를 보내 사용한다. 이러한 query가 SQL로 번역된다.select
의 대상이 특정 attribute가 아닌 Entity 자체이다.JPQL이라는 객체 지향 쿼리를 사용한다.
@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();
}
PK 기반의 query가 아닌 경우에는 JPQL을 작성해야 한다.
JPA는 모든 데이터의 변경이 트랜잭션 안에서 이뤄져야 한다. 따라서 JPA를 사용하기 위해서는 항상 Transactional이 존재해야 한다.
service/MemberService.java
에 @Transactional
을 추가하는데,
class 자체에 해줘도 되고, 회원 가입 시에만 동시성 제어가 필요하므로 Long join()
method에만 추가해줘도 된다.
이제 SpringConfig
에 설정을 추가해줘야 한다.
private EntityManager em;
@Autowired
public SpringConfig(EntityManager em) {this.em = em;}
@Bean
public MemberRepository memberRepository() {
//return new JdbcMemberRepository(dataSource);
//return new JdbcTemplateMemberRepository(dataSource);
return new JpaMemberRepository(em);
}
JpaMemberRepository
는 EntityManager
만을 필요로 하기에, 기존의 DataSource
와 생성자는 삭제한다.이제 이전에 구현했던 스프링 통합 Test 로 정상 동작하는지 확인한다.
자꾸 에러가 난다...
어딘가 수정이 필요하다.
Hibernate: select member0_.id as id1_0_, member0_.name as name2_0_, member0_.phone as phone3_0_, member0_.pwd as pwd4_0_ from member member0_ where member0_.name=?
Hibernate: select member0_.id as id1_0_, member0_.name as name2_0_, member0_.phone as phone3_0_, member0_.pwd as pwd4_0_ from member member0_ where member0_.name=?
Hibernate: insert into member (id, name, phone, pwd) values (null, ?, ?, ?)
2022-01-11 15:51:29.818 WARN 2434 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 23502, SQLState: 23502
2022-01-11 15:51:29.818 ERROR 2434 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : NULL not allowed for column "ID"; SQL statement:
Test method에 @Commit
annotation을 사용하면, @Transactional
이 있어도 rollback되지 않고 DB에 반영된다.
ERROR 수정
https://www.inflearn.com/questions/389513 해당 링크를 보면서,
- h2 DB를 낮은 버전으로 새로 받고, 새 db를 만들어서 table을 생성
- 이후 실행했을 때 방금 생성한
Member
table을 못 찾는다는 에러가 떠서application.properties
에서spring.jpa.hibernate.ddl-auto = create
로 수정- 에러 fix 완료 ...
스프링 데이터 JPA를 이용하면, 이전에 구현한 Repository에 구현체 없이도 Interface만으로 개발이 가능하다. (CRUD 기능도 제공한다!)
JPA를 편리하게 사용하도록 해주는 기술이므로 JPA에 대한 이해와 학습이 수반되어야 한다!
repository/SpringDataJpaRepository.java
(interface!)
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
@Override
Optional<Member> findByName(String name);
}
extends
를 사용하고 다중 상속이 가능하다.JpaRepository<Member, Long>
Member
: 사용하는 EntityLong
: Entity의 식별자 (PK, id
)JpaRepository
를 상속받아 사용하는 interface를 발견하면, SpringDataJpaRepository
가 알아서 구현체를 만들어 스프링 빈에 등록하도록 한다. 그래서 이를 SpringConfig
를 통해 가져다 사용하면 된다.SpringConfig
private final MemberRepository memberRepository;
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository);
}
// @Bean
// public MemberRepository memberRepository() {
// //return new JdbcMemberRepository(dataSource);
// //return new JdbcTemplateMemberRepository(dataSource);
// //return new JpaMemberRepository(em);
// }
MemberService
에 의존 관계를 설정한다.SpringConfig
생성자로 인해 MemberRepository
를 스프링 컨테이너에서 탐색하지만, 사용자가 등록한 것은 없다.
JpaRepository
를 상속받는 interface를 통해 스프링 데이터 JPA가 구현체를 생성해 스프링 빈으로 등록한다.MemberRepository
를 MemberService
에도 그대로 전달해 사용한다.테스트해보면 전과 같은 에러가 뜬다..............
JpaRepository
를 까보면,
JpaRepository
가 상속받는 상위 Repository들을 확인해보면, PagingAndSortingRepository
(Paging 기능), CrudRepository
가 있다.Optional<Member> findByName(String name);
과 같이 interface에 기술해야 한다.findBy{ }And{ }(String { }, Long { })
과 같은 형태여야 한다.실무에서는 JPA 와 스프링 데이터 JPA 를 기본으로 사용하고, 복잡한 동적 query는 Querydsl 이라는 라이브러리를 사용한다. 이는 자바 코드로 안전하게 query를 작성할 수 있고, 동적 query 또한 편리하게 작성하도록 돕는다.
개방-폐쇄 원칙, 다형성, @Transactional, JPA