스프링 입문-웹 mvc와 DB연동
웹 mvc 구축
실제로 웹 내에서 데이터를 입력하여 서버에 저장하는 model view controller를 만들어 보았다.
@GetMapping(value = "/members/new")
public String createForm() {
return "members/createMemberForm";
}
@PostMapping(value = "/members/new")
public String create(MemberForm form) {
Member member = new Member();
member.setName(form.getName());
memberService.join(member);
return "redirect:/";
}
위의 두 메서드는 모두 “/members/new”로 맵핑되어있다. 하지만 GetMapping과 PostMapping으로 방식이 서로 다른데 GET과 POST는 이전에 배웠던 HTTP 메서드를 뜻한다. 따라서
<!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>
일반적으로 해당 url로 들어갔을 때에는 GET을 사용하므로 GetMapping으로 맵핑된 메서드가 작동하여 위의 html을 화면에 띄워준다.
템플릿 엔진이 렌더링한 화면에 input을 입력하고 등록 버튼을 누르면 해당 메서드는 post로 지정되있으므로 PostMapping이 발동하여 입력된 정보로 멤버를 만들어 서버에 저장하고 홈으로 리다이렉트 해준다.
DB 연동
이후 h2 데이터베이스를 설치하여 해당 DB와 웹을 연동하는 작업을 하였다. 순수 Jdbc 파트는 너무 길고 난해하므로 일단 넘어가고 스프링이 제공하는 JdbcTemplate을 사용한 파트만 서술하겠다.
package hello.hello_spring.repository;
import hello.hello_spring.domain.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class JdbcTemplateMemberRepository implements MemberRepository{
private final JdbcTemplate jdbcTemplate;
@Autowired
public JdbcTemplateMemberRepository(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}//DB와 연결된 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;
}//insert문을 작성할 필요 없이 java코드로 DB에 데이터를 저장할 수 있다
@Override
public Optional<Member> findById(Long id) {
List<Member> result = jdbcTemplate.query("select * from member where id=?", memberRowMapper(),id);
return result.stream().findAny();
}
@Override
public Optional<Member> findByName(String name) {
List<Member> result = jdbcTemplate.query("select * from member where name=?", memberRowMapper(),name);
return result.stream().findAny();
}
@Override
public List<Member> findAll() {
return jdbcTemplate.query("select * from member", memberRowMapper());
}
private RowMapper<Member> memberRowMapper() {
return new RowMapper<Member>() {
@Override
public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
Member member=new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
}
};
}//JdbcTemplate을 사용하기 위해서 Member 객체를 RowMapper로 wrapping 해주는 작업이 필요하다.
}
위의 코드가 JdbcTemplate을 사용하여 DB와 연동된 작업을 수행하는 코드이다.
이러한 코드의 대대적인 수정이 있었음에도 우리는 java의 객체지향이 제공하는 다형성의 원리와, Spring이 제공하는 DI(Dependency Injection)을 통해 조립하는 몇 줄의 코드만 고쳐서 그대로 지금의 코드를 사용할 수 있다.
우리는 config에서 스프링 빈을 등록하며 MemberService가 스프링 빈으로 등록된 인터페이스 MemberRepository의 구현체에 의존하도록 DI를 구성하였다.
따라서 우리는 지금까지 사용했던 MemoryMemberRepository 대신 같은 인터페이스의 구현체인 JdbcTemplateMemberRepository를 스프링 빈에 등록시켜 주기만 하면 내부 기능은 확연하게 달라졌지만 MemberService를 뜯어고칠 필요 없이 그대로 같은 클래스인 것처럼 사용이 가능하다.
이는 객체지향의 SOLID 원칙 중 OCP, 개방-폐쇄 원칙을 지킨 것으로 ‘확장에는 열려있고, 수정, 변경에는 닫혀있다’는 원칙을 준수한 것이다. 풀이하자면 기존 구성요소의 기능을 확장시키는 것은 자유롭게 가능하게, 그러면서 확장에 따른 기존 구성요소의 수정은 최소화하도록 한다는 뜻이다.
참고로 DB와 연동하여 통합 테스트(Integration Test)를 진행할 때에는 클래스 위에 @SpringBootTest, @Transactional 두 어노테이션을 붙여줘야 하는데, @SpringBootTest는 스프링 컨테이너와 함께 테스트를 진행한다는 뜻이고, @Transactional은 테스트 시작 전에 트랜잭션을 실행하고, 테스트가 끝난 후에는 롤백한다는 뜻으로 테스트가 저장된 데이터에 아무런 영향을 끼치지 않도록 할 수 있다.
JPA
JPA를 사용하면 반복적인 코드들, 기초적인 SQL들을 JPA가 자동으로 처리해준다. JPA는 표준 인터페이스라고 할 수 있고 이러한 인터페이스들을 구현하는 Hybernate 등의 프레임워크들이 있다. JPA는 ORM의 일종으로 Object Relational Mapping, 즉 객체를 RDB와 맵핑시켜주는 역할을 한다.
package hello.hello_spring.repository;
import hello.hello_spring.domain.Member;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
public class JpaMemberRepository implements MemberRepository{
private final EntityManager em;
public JpaMemberRepository(EntityManager em) {
this.em = em;
}
@Override
public Member save(Member member) {
em.persist(member);
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();
}
}
JPA를 사용해 다시 작성한 리포지토리이다. JPA에서 제공하는 EntityManager와 JPQL을 사용하여 기존의 복잡했던 쿼리들이 굉장히 간결해진 것을 볼 수 있다. 참고로 JPA를 통한 모든 변경은 @Transactional 내부에서 이루어져야 하므로 Service 클래스에 어노테이션을 달아주어야 한다.
스프링 데이터 JPA
JPA도 충분히 편리하지만 이러한 JPA를 더 쉽게 사용할 수 있는 프레임워크가 ‘스프링 데이터 JPA’이다. 스프링 데이터 JPA를 사용하면 프레임워크 내부에서 기본적으로 제공하는 여러 메서드로 기본적인 CRUD를 포함하여 여러 반복적인 코드들을 구현하지 않고 인터페이스만 사용하여 구현을 완료할 수 있다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
Optional<Member> findByName(String name);
}
지금까지 리포지토리에서 구현했던 findByName 기능을 인터페이스 내부에서 추상메서드만 작성하는 것으로 끝냈다. config에서 리포지토리를 이것으로 바꾸고 테스트를 실행시키면 문제없이 돌아간다.
이런 일이 가능한 이유는 위와 같이 프레임워크 내에 CRUD에 자주 사용되는 메서드들이 이미 구현되어 있기 때문이다. 또한 일정 규칙대로 ‘findBY???’을 메서드 이름으로 사용하면 뒤의 ???에 무엇을 적느냐에 따라 각자의 프로그램 내에서 정의한 객체의 멤버를 조회가 가능하다.
스프링 데이터 JPA는 어디까지나 JPA를 편리하게 사용하게 해주는 프레임워크인 만큼 JPA의 공부가 선행되어야 한다.