인프런 강의 김영한
회원 관리 예제 - 웹 MVC 개발
16. 회원 웹 기능 - 홈 화면 추가
멤버컨트롤러로 회원을 등록하고 조회하기
아주 단순한 버튼잇는 사이트
@Controller
public class HomeController {
@GetMapping("/")
public String home() {
return "home";
}
}<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
<div>
<h1>Hello Spring</h1>
<p>회원 기능</p>
<p>
<a href="/members/new">회원 가입</a>
<a href="/members">회원 목록</a>
</p>
</div>
</div>
<!-- /container -->
</body>
</html>static에 index만들엇는데 아무것도 없으면 여기가 호출된다.
가장먼저 스프링 컨테이너 뒤지고 없으면 static 찾는다.
매핑된게 잇으니 home이먼저 보여지는 것이다.
17. 회원 웹 기능 - 등록
17.1 컨트롤러
멤버 등록 폼으로 가지기
@GetMapping(value = "/members/new")
public String createForm() {
return "members/createMemberForm";
}17.2 view
이름을 넘기는 폼
<!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>17.3 웹 등록 화면에서 데이터를 전달 받을 폼 객체
public class MemberForm {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}17.4 회원 컨트롤러에서 회원을 실제 등록하는 기능
post매핑으로 값을 받아와서 처리한다.
@PostMapping(value = "/members/new")
public String create(MemberForm form) {
Member member = new Member();
member.setName(form.getName());
memberService.join(member);
return "redirect:/";
}18. 회원 웹 기능 - 조회
18.1 컨트롤러
멤버 리스트를 만들어서 view로 보내기
@GetMapping(value = "/members")
public String list(Model model) {
List<Member> members = memberService.findMembers();
model.addAttribute("members", members);
return "members/memberList";
}18.2 view
th:each="member : ${members}"
타임리프:each="var : ${items}"이 느낌으로 넣어주는 듯하다.
탬플릿이 model에담은것을 불러와서 루프로 보내준다.
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
<div>
<table>
<thead>
<tr>
<th>#</th>
<th>이름</th>
</tr>
</thead>
<tbody>
<tr th:each="member : ${members}">
<td th:text="${member.id}"></td>
<td th:text="${member.name}"></td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- /container -->
</body>
</html>19. H2 데이터베이스 설치
지금까지 방식은 메모리안에 있는것이기때문에 다 사라진다.
그래서DB에 저장을해야한다.
아주 가볍고 심플한 H2데이터베이스를 사용해보도록 하자.
h2다운 - bin ./h2.sh로 h2.sh실행
윈도우는 그냥 h2.bat실행하면된다.
그러면 웹브라우저에 db뜬다.
그냥두고 연결
사용자폴더에 /test.mv.db파일 생성확인
이후로부터는 오류가 날 수 잇어서
jdbc:h2:tcp://localhost/~/test로 접속하기
member테이블을 만들어주자.
create table member
(
id bigint generated by default as identity,
name varchar(255),
primary key (id)
);20 순수 JDBC
먼저 build.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리 추가해줘야한다
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'접속정보 등록해주기 mysql이나 오라클이나 비슷하다.
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa이렇게하면 스프링이 db와 연결할 준비를 알아서 해준다.
MemberRepository를 인터페이스로 만들고 MemoryMemberRepository로 메모리에 맵으로 저장하는 것을 구현햇엇다.
다형성을 이용해서 jdbcMemberRepository를 만들고 주입하는 것을 갈아 끼워주기만 하면된다.
db를 사용하려면 DataSource가 필요하다.
이것을 주입받으면된다.
생성자를 통해서 주입받자
.
dataSource로 커넥션을 얻고 statement로 값을 넘겨준다.
Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, member.getName());
pstmt.executeUpdate();dataSource에 직접 getconnectin할 수잇는데 스프링을 통해 연결하면 DataSourceUtils으로 연결할 수있다
public class JdbcMemberRepository implements MemberRepository {
private final DataSource dataSource;
public JdbcMemberRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Member save(Member member) {
String sql = "insert into member(name) 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.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);
}
}
// 다른 것들
private Connection getConnection() {
return DataSourceUtils.getConnection(dataSource);
}
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);
}
}과거에는 각각 알아서 조립을 햇어야햇는데 SpringConfig에서 di를 할 수 있다.
dataSource에 연결정보를 di해주고
MemberRepository에 MemoryMemberRepository대신 JdbcMemberRepository를 연결해줘야한다.
스프링이 알아서 정보를 보고 만들어주고 알아서 주입을 해준다.
어떤 코드도 만지지 않고 확장을 하고 config만 했다.
@Configuration
public class SpringConfig {
private final DataSource dataSource;
@Autowired
public SpringConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
// return new MemoryMemberRepository();
return new JdbcMemberRepository(dataSource);
}
}스프링을 왜 쓰냐? DI로 이런 다형성을 잘 구현하기 때문이다
과거에는 하나를 수정하면 각 코드로 가서 수정을 햇엇다.
이제는 기존의 코드는 손대지 않고 CONFIG만 손대서 조립하면된다.
이걸 굉장히 편리하게 해주는게 스프링의 장점이다.
객체지향의 진짜 장점이자 매력이 이런점이다.
20. 스프링 통합 테스트
스프링 컨테이너와 DB까지 연결한 통합 테스트를 진행해보자
이전에 한거는 스프링과 관련없는 순수한 자바코드 테스트엿다.
이제 스프링과 db와 연결되엇으니 스프링과 관련해서 테스트를 해야한다.
MemberServiceIntegrationTest 테스트 클래스를 만들어서 진행해보자.
스프링테스트를 하려면 @SpringBootTest어노테이션을 붙여줘야한다.
직접 객체를 생성해서 넣엇엇는데 @Autowired로 객체를 스프링이 넣어서 심어주게하자.
보통 테스트 전용 DB를 만들어서 테스트를 한다.
그런데 테스트는 반복할 수 있어야한다.
테스트마다 DB에 들어가면 반복을 할 수 없다.
@Transactional어노테이션을 넣어주면 트랜젝션 처리가 된다.
테스트 끝나고 롤백해버리면된다.
이러면 다음 테스트를 다시 해볼 수 있는 것이다.
@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {
@Autowired
MemberService memberService;
@Autowired
MemberRepository memberRepository;
@Test
void join() {
// given
Member member = new Member();
member.setName("spring");
// when
Long saveId = memberService.join(member);
// then
Member findMember = memberService.findOne(saveId).get();
Assertions.assertEquals(member.getName(), findMember.getName());
}
}결론적으로
@SpringBootTest
스프링 컨테이너와 테스트를 함께 실행한다.
@Transactional
테스트 케이스에 이 애노테이션이 있으면
테스트 시작 전에 트랜잭션을 시작하고 테스트 완료 후에 항상 롤백한다.
이렇게 하면 DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지않는다.
참고로 @Commit하면 커밋까지 해준다.
자바코드로 한것은 단위테스트라고한다. 최소한의 단위만 한다.
스프링까지 포함해서 스프링뜨고하는 것은 통합테스트라고 한다.
순수한 단위테스트가 더 좋을 수도 있고 스프링이 꼭 더 좋은 테스트라고 할 수는 없다.
진짜 좋은 것은 단위테스트를 더 잘 만드는게 좋다.
21. 스프링 JdbcTemplate
순수 Jdbc와 동일한 환경설정을 하면 된다.
스프링 JdbcTemplate과 MyBatis 같은 라이브러리는
JDBC API에서 본 반복 코드를 대부분 제거해준다.
SQL은 직접 작성해야 한다
JdbcTemplate jdbcTemplate를 써야하고 얘를 인젝션받을수는없다.
데이터소스 인젝션받앗던 것을 생성자에 넣어주면된다.
private final JdbcTemplate jdbcTemplate;
@Autowired
public JdbcTemplateMemberRepository(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}jdbcTemplate.query("쿼리문", memberRowMapper(), 파라미터);
결과나오는 것을 memberRowMapper로 매핑해줘야한다.
@Override
public Optional<Member> findById(Long id) {
List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(), id);
return result.stream().findAny();
}
private RowMapper<Member> memberRowMapper() {
return (rs, rowNum) -> {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
};
}save는 있는 member객체로 알아서 매핑해주게 할 수 있다.
@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;
}직접 웹에서 볼필요가 없고 만들어놓은 서비스 테스트를 사용하면된다.
잘되면 db연동이 잘된 것이다!!
테스트코드를 잘짜야한다.
22. JPA
JDBC탬플릿과 마이바티스는 중복되는 코드들을 줄여주는 역할을 한다.
그렇지만 개발자가 쿼리를 작성해야햇엇다.
JPA는 쿼리 자체도 없애준다. 개발생산성을 높일 수 있다.
메모리에 집어넣듯이 알아서 보내고 알아서 처리해준다.
단순히 줄이는것에서 넘어서 SQL과 데이터 중심의 설계에서 객체 중심의 설걔로 파라다임을 전환할 수 있다.
자바 퍼시스턴스 API의 줄임말이다.
스프링 프레임워크 정말 큰 기술이다. JPA도 스프링만큼 깊이가 있는 것이다.
스프링에서도 많이 지원하고 있다.
먼저 라이브러리를 추가해줘야한다.
JDBC, JPA등등 다 포함하는 라이브러리이다.
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'또한 application.properties에서 설정을 해줘야한다.
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=noneshow-sql : JPA가 생성하는 SQL을 출력한다.
ddl-auto : JPA는 테이블을 자동으로 생성하는 기능을 제공하는데 none 를 사용하면 해당 기능을 끈다.
ddl-auto=create를 하면 엔티티 정보를 바탕으로 테이블도 직접 생성해준다.
JPA는 인터페이스이고 구현체로 여러 VENDOR를 사용해야한다.
JPA는 자바 표준 인터페이스이고 구현은 여러업체마다 구현을 하고있다.
객체는 ORM이다 객체와 데이터테이블을 매핑한다.
@Entity 어노테이션을 붙이면 객체와 테이블을 매핑해준다.
DB가 알아서 생성해주는것은 다음처럼 해줘야한다.
컬럼명에 대해서는 @Column어노테이션을 사용해서매핑을 해줘야한다.
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Entity
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
//@Column(name = "username")
private String name;
} JpaMemberRepository에서는 EntityManager객체를 만들어서 사용하면된다.
jpa는 EntityManager로 모든 것이 동작한다.
boot가 알아서 만들어놓기 때문에 DI하기만 하면된다.
insert는 persist()메소드를 사용하면 영속하다 뜻을 가지고 있는데 insert쿼리만들어서 알아서 setid까지 해준다.
pk로 찾는것은 find(찾을 객체, 파라미터)메소드를 사용하면된다.
name과 같은 파라마터로 찾는 것은 특별한 객체 지향 쿼리언어를 사용해야한다.
select m from Member m", Member.class
Member entity를 대상으로 쿼리를 날리는 것이다.
객체를 대상으로 쿼리를 날린다.
select하는데 멤버 객체 자체를 select하는 것이다.
JPA를 사용하려면 항상 트랜젝션이 되어야한다.
config에서는 EntityManager을 제공해서 생성해야한다.
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);
}
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();
}
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
}
@Configuration
public class SpringConfig {
@Autowired
private EntityManager em;
@Bean
public MemberRepository memberRepository() {
return new JpaMemberRepository(em);
}
}Hibernate구현체가 생성되서 테스트된다.
Hibernate: insert into member (id, name) values (default, ?)
Hibernate: select m1_0.id,m1_0.name from member m1_0 where m1_0.name=?
최근 jpa많이 사용하고 있다. 그런데 스프링만큼 깊이가 잇어서 따로 공부를 해야한다.
23. 스프링 데이터 JPA
스프링 부트와 JPA만 사용해도 개발 생산성이 정말 많이 증가하고 개발해야할 코드도 확연히 줄어든다.
여기에 스프링 데이터 JPA를 사용하면
리포지토리에 구현 클래스 없이 인터페이스 만으로 개발을 완료할 수 있다
그리고 반복 개발해온 기본 CRUD 기능도 스프링 데이터 JPA가 모두 제공한다.
지금까지 조금이라도 단순하고 반복이라 생각했던 개발 코드들이 확연하게 줄어든다.
따라서 개발자는 핵심 비즈니스 로직을 개발하는데 집중할 수 있게 된다.
실무에서 관계형 데이터베이스를 사용한다면 스프링 데이터 JPA는 이제 선택이 아니라 필수?
주의: 스프링 데이터 JPA는 JPA를 편리하게 사용하도록 도와주는 기술이다.
따라서 JPA를 먼저 학습한 후에 스프링 데이터 JPA를 학습해야 한다.
앞의 JPA 설정을 그대로 사용한다.
JpaRepository와 MemberRepository를 상속받아야한다. 인터페이스는 다중상속이 가능하다.
구현체를 자동으로 만들어서 알아서 등록해준다.
@Configuration
public class SpringConfig {
private final MemberRepository memberRepository;
@Autowired
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Bean
public MemberService memberService() {
//return new MemberService(memberRepository());
return new MemberService(memberRepository);
}
}스프링 데이터 JPA 제공 기능은 다음과 같다.
인터페이스를 통한 기본적인 CRUD
findByName() , findByEmail()처럼 메서드 이름 만으로 조회 기능 제공
페이징 기능 자동 제공한다
인터페이스 이름만으로도 끝낼 수 있게 되는 것이다.
머리속으로 상상할수 잇는 공통화된것은 이미 완성되어 있다.
-> jpa쓰려고 처음부터 시그니쳐를 맞춰놓은거였다.
참고: 실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용하고
복잡한 동적 쿼리는 Querydsl이라는 라이브러리를 사용하면 된다.
Querydsl을 사용하면 쿼리도 자바 코드로 안전하게 작성할 수 있고,
동적쿼리도 편리하게 작성할 수 있다.
이 조합으로 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리를 사용하거나
앞서 학습한 스프링 JdbcTemplate를 사용하면 된다
jpa + 마이바티스 이런것도 가능하다.
aop
24 AOP가 필요한 상황
이론적으로 공부를 하면 이해하기 어렵다.
모든 메소드의 호출 시간을 측정하고 싶다면?
단순하게하면 로직마다 시간측정을 추가하면된다.
// 회원가입
public Long join(Member member) {
long start = System.currentTimeMillis();
try {
validateDuplicateMember(member); // 중복 회원 검증
memberRepository.save(member);
return member.getId();
} finally {
long end = System.currentTimeMillis();
long timeMs = end - start;
System.out.println("join= " + timeMs + "ms");
}
}그런데 이것를 모든 메소드마다 넣는 것은 매우 복잡하다.
또 필요없으면 삭제를 해줘야한다.
문제점은 다음과 같다.
회원가입, 회원 조회에 시간을 측정하는 기능은 핵심 관심 기능이 아니다.
곁다리 로직이다.
시간을 측정하는 로직은 공통 관심 사항이다.
시간을 측정하는 로직과 핵심 비즈니스의 로직이 섞이면 유지보수가 어렵다.
시간을 측정하는 로직을 별도의 공통 로직으로 만들기 매우 어렵다.
시간을 측정하는 로직을 변경할 때 모든 로직을 찾아가면서 변경해야 한다
공통 관심 사항(cross-cutting concern)과 핵심 관심 사항(core concern)을 분리할 필요가 생기게 되는 것이다.
여기서 사용되는 것이 AOP이다.
25. AOP 적용
AOP: Aspect Oriented Programming 관점지향프로그래밍
시간측정 로직을 모으고 원하는 곳에 적용한다.
스프링에서는 쉽게 사용하도록 제공한다.
클래스에 @Aspect어노테이션을 달아줘야한다.
@Around어노테이션으로 타게팅을 해줄 수 있다.
패키지명..클래스명..메소드명등 원하는 조건을 넣을 수 있다.
joinPoint에다가 호출이 될때마다 파라미터 를 넣어준다. 조작하거나 다음 메소드를 거치게 하는 등의 역할을 한다.
중간에 인터셉트해서 사용하게 된다.
@Aspect
@Component
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");
}
}
}해결
회원가입, 회원 조회등 핵심 관심사항과 시간을 측정하는 공통 관심 사항을 분리한다.
시간을 측정하는 로직을 별도의 공통 로직으로 만들었다.
핵심 관심 사항을 깔끔하게 유지할 수 있다.
변경이 필요하면 이 로직만 변경하면 된다.
원하는 적용 대상을 선택할 수 있다. @Arround 어노테이션을 검색해서 api를 보고 조정하자.
AOP를 적용하면 가짜 서비스 프록시를 만들고 끝나면 진짜를 호출해준다.
26. 강의의 끝
지금까지 스프링으로 웹 애플리케이션을 개발하는 방법에 대해서 얇고 넓게 학습했다.
이제부터는 각각의 기술들을 깊이있게 이해해야 한다.
거대한 스프링의 모든 것을 세세하게 알 필요는 없다.
우리는 스프링을 만드는 개발자가 아니다.
스프링을 활용해서 실무에서 발생하는 문제들을 잘 해결하는 것이 훨씬 중요하다.
따라서 핵심 원리를 이해하고, 문제가 발생했을 때, 대략 어디쯤 부터 찾아들어가면 될지,
필요한 부분을 찾아서 사용할 수 있는 능력이 더 중요하다
실무에 가깝게 배우는 것!
2023.05.09
기본적인 강의 이기도 하지만 프로그래밍에 대한 개념자체를 상기 시켜주는 것 같다.
앞으로는 진도를 나가고 있지만 복습과 개념의 이해를 위해 나아가는 것도 좋아보인다.
'기초단계 > SPRING' 카테고리의 다른 글
| 2023.05.11 Spring (0) | 2023.05.11 |
|---|---|
| 2023.05.10 Spring (0) | 2023.05.11 |
| 2023.05.04 Spring (0) | 2023.05.09 |
| 2023.05.02 Spring 김영한 (0) | 2023.05.02 |
| 2023.04.28 Spring (0) | 2023.04.28 |