등록 및 조회
- MemberController
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
31
32
@Controller
public class MemberController {
private final MemberService memberService;
@Autowired
public MemberController(MemberService memberService){
this.memberService = memberService;
}
@GetMapping("/members/new")
public String createForm(){
return "members/createMemberForm";
}
@PostMapping("/members/new") // 등록
public String create(MemberForm form){
Member member = new Member();
member.setName(form.getName());
memberService.join(member);
return "redirect:/";
}
@GetMapping("/members") // 조회
public String list(Model model){
List<Member> members = memberService.findMembers();
model.addAttribute("members", members);
return "members/memberList";
}
}
- MemberForm
1
2
3
4
5
6
7
8
9
10
11
public class MemberForm {
private String name;
public String getName(){
return name;
}
public void setName(String name) {
this.name = name;
}
}
- createMemberForm.html
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
<form action="/members/new" method="post">
<div class="form-group">
<label for="name">이름</label>
<input type="text" id="name" name="name" placeholder="이름을
입력하세요">
</div> <button type="submit">등록</button>
</form></div> <!-- /container -->
</body>
</html>
** form 태그의 method = “post” action = “/members/new”
form을 제출 했을 때 해당 URL에 post방식으로 전송한다.
@PostMapping
@PostMapping: 데이터를 Form같은 곳에 넣어서 전달할 때 사용.
데이터를 등록할 때 사용하게 된다.
@GetMapping은 주로 조회할 때 사용한다.
DB 접근
환경설정
1
2
3
4
//build.gradle
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
1
2
3
4
5
//resources/application.properties에
spring.datasource.url=jdbc:h2:tcp://localhost/~/test //h2 db 열 때 사용했던 주소
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa //h2 db의 username
JdbcTemplate (얘도 실무에서 많이 사용 함)
스프링 JdbcTemplate와 MyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거해준다. 하지만 SQL은 직접 작성해야 한다.
- JdbcTemplateMemberRepository
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class JdbcTemplateMemberRepository implements MemberRepository{
private final JdbcTemplate jdbcTemplate;
@Autowired
public JdbcTemplateMemberRepository(DataSource dataSource){
/*DataSource는 데이터베이스 커넥션을 획득할 때 사용하는 객체이다.
스프링 부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어둔다.*/
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@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());
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());
return result.stream().findAny();
}
@Override
public Optional<Member> findByName(String name) {
List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper());
return result.stream().findAny();
}
@Override
public List<Member> findAll() {
return jdbcTemplate.query("select * from member", memberRowMapper());
}
private RowMapper<Member> memberRowMapper(){
return (rs, rowNum) -> {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
};
}
}
- SpringConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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 JdbcTemplateMemberRepository(dataSource); // new MemoryMemberRepository();로 메모리에 접근했던 부분을 이렇게 이 부분만 수정하면 나머지는 수정 없이 반영 된다.
}
}
스프링 부트 + JPA
- JPA는 기존의 반복 코드와 기본적인 SQL도 만들어서 실행해준다.
- JPA를 사용하면, SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환할 수 있다.
- JPA를 사용하면 개발 생산성을 크게 높일 수 있다.
build.gradle의 dependencies에 추가.
1
2
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
application.properties에 추가. (URL은 개인별 DB)
1
2
3
4
5
6
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
- show-sql: JPA가 생성하는 SQL을 출력한다.
- ddl-auto: JPA는 테이블을 자동으로 생성하는 기능을 제공하는데 none을 사용하면 해당 기능을 끈다.
- create를 사용하면 엔티티 정보를 바탕으로 테이블도 직접 생성해준다.
@Entity, @Id, @Column
- @Entity 어노테이션을 통해 Member domain을 DB 테이블과 연결할 수 있다.
- @Id @GeneratedValue(strategy = GenerationType.IDENTITY) 어노테이션을 통해 자동으로 DB가 생성하는 ID를 매핑할 수 있다.
- @Column(name = “username”) String name; 이면 name을 DB의 username 컬럼에 매핑한다.
- Model/Member
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Entity
public class Member{
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) //자동으로 생성하는 id와 매핑
private Long id;
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;
}
- JpaMemberRepository
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
31
32
33
34
35
36
public class JpaMemberRepository implements MemberRepository{
private final EntityManager em;
//현재 DB와 연결해서 관리를 하는 EntityManager를 스프링부트에서 생성해주고 얘를 주입받아 사용해야 함.
public JpaMemberRepository(EntityManager em){
this.em = em;
}
@Override
public Member save(Member member) {
em.persist(member); //영구 저장하다 => persist
return member;
}
@Override
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id);
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() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
}
** EntityManaber em: JPA를 이용하려면 주입받아야 함.
스프링 데이터 JPA
스프링 데이터 JPA 프레임워크를 사용하면 리포지토리에 구현 클래스 없이 인터페이스만으로 개발을 할 수 있다. 기본 CRUD 기능도 스프링 데이터 JPA가 모두 제공한다.
** CRUD = Create, Remove, Update, Delete
- SpringDataJpaMemberRepository
1
2
3
4
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
@Override
Optional<Member> findByName(String name); //구현클래스 없어도 사용 가능.
}
- SpringConfig
1
2
3
4
5
6
7
8
9
10
@Configuration
public class SpringConfig {
private final MemberRepository memberRepository;
@Autowired
public SpringConfig(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
}
** extends JpaRepository<Member, Long> 에서 Member는 해당 객체 Long은 id, 즉 식별자의 Type.
** 서비스 계층에 @Transactional 어노테이션 추가해야 한다.
- JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행해야 한다.
- 인터페이스를 통해 기본적인 CRUD 제공
- findByName(), findByEmail() 등 메서드 선언만으로 기능 제공
- 페이징 기능 자동 제공
스프링 통합테스트
스프링 컨테이너와 DB까지 연결한 통합테스트
- Test.MemberServiceIntegrationTest
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
31
32
@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test public void 회원가입() throws Exception {
//Given
Member member = new Member();
member.setName("hello");
//When
Long saveId = memberService.join(member);
//Then
Member findMember = memberRepository.findById(saveId).get();
assertEquals(member.getName(), findMember.getName());
}
@Test
public void 중복_회원_예외() throws Exception {
//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("이미 존재하는 회원입니다."); }
}
** 이제 DB와 연결되었기 때문에 @AfterEach로 clear를 할 필요 없고(@Transactional 덕분) 스프링 빈으로 객체들을 관리하기 때문에 @Autowired를 사용해 객체를 내려받는다. @BeforeEach로 하나의 객체를 참조할 필요 없다.
@SpringBootTest, @Transactional
- @SpringBootTest: 스프링 컨테이너와 테스트를 함께 실행한다.
- @Transactional: 테스트 케이스에 이 어노테이션이 있으면, 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 롤백한다. DB에 데이터가 남지 않으므로 이 어노테이션 덕분에 @AfterEach를 하지 않아도 되는 것이다. (Service 등에 붙으면 롤백하지 않는다.)
** SQL이 작동하고 commit이 되어야 DB에 반영이 되는 구조. Test 하는 메서드에 @Commit을 붙이면 commit을 하게 된다.
** 이전에 한 테스트가 순수 자바코드를 이용한 단위 테스트이고 단위 테스트를 잘 만드는 것이 더 좋을 확률이 높다.
AOP
AOP = Aspect Oriented Programming
- AOP가 필요한 상황
- 모든 메서드들의 호출 시간을 측정하고 싶다면?
- 공통 관심 사항 (cross-cutting concern) vs 핵심 관심 사항 (core concern)
- 회원 가입 시간, 회원 조회 시간을 측정하고 싶다면?
- Aop.TimeTraceAop
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Aspect
public class TimeTraceAop {
@Around("execution(* hello.hellospring..*(..)")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable{
long start = System.currentTimeMillis();
System.out.println("START: " + joinPoint.toString());
try{
return joinPoint.proceed();
}finally {
long finish = System.currentTimeMillis();
long timeMs = finish - start;
System.out.println("END: " + joinPoint.toString() + " " + timeMs + "ms");
}
}
}
** @Aspect 어노테이션 사용해야 함.
@Aspect, @Around
@Around 어노테이션으로 Aop 적용 범위 지정 hello.hellospring 이라는 프로젝트 명 하위의 모두에게 적용한다는 뜻 ex) hello.hellospring.service.. 이면 service 하위에만 찍힘.
SpringConfig에서 Bean 등록해야 함.
1
2
3
4
@Bean
public TimeTraceAop timeTraceAop(){
return new TimeTraceAop();
}