[인프런]스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 강의 정리(2)

화나·2020년 11월 5일
0
post-thumbnail

이 게시글은 [인프런]스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 강의를 기반으로 작성되었으며 강의를 기억하기 위한 기록물입니다.
출처 : 인프런

4. 스프링 빈과 의존관계

4-1. 컴포넌트 스캔과 자동 의존관계 설정

package hello.hellospring.controller;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
     public class MemberController {
     
     private final MemberService memberService;
     
     @Autowired
     public MemberController(MemberService memberService) {
       	this.memberService = memberService;
     }
}
  • 생성자에 @Autowired가 있으면 스프링이 연관된 객체를 스프링 컨테이너에서 찾아서 넣어준다.
  • 이렇게 의존관계를 개발자가 직접 주입하는게 아니라, 외부에서 주입하는것을 DI(Dependency Injection, 의존성주입)이라고 한다.
  • 컴포넌트 스캔의 원리
    • @Component 애노테이션이 있으면 스프링 빈으로 자동 등록된다.
    • @Controller, @Service, @Repository 애노테이션들도 @Component를 포함하고 있기 때문에 스프링 빈으로 자동 등록된다.

4-2. 자바코드로 직접 스프링 빈 등록하기

package hello.hellospring;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import hello.hellospring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {
   
   @Bean
   public MemberService memberService() {
   		return new MemberService(memberRepository());
   }
  
  @Bean
   public MemberRepository memberRepository() {
  		return new MemoryMemberRepository();
   }
}
  • 회원 서비스와 리포지토리에 애노테이션을 제거하고 진행한다.
  • DI는 필드 주입, setter 주입, 생성자 주입 3가지 방법이 있는데, 의존 관계가 실행 중 동적으로 변하는 경우는 거의 없으므로 생성자 주입을 권장한다.
  • 주로 정형화된 컴포넌트 스캔을 사용하지만, 정형화 되지않거나 상황에 따라 구현 클래스를 변경해야 할 경우는 설정을 통해 스프링 빈으로 등록한다.
  • 해당 프로젝트는 향후 리포지토리를 다른 리포지토리로 변경한 예정이므로 스프링 빈 방식으로 설정을 한다.

5. 회원관리 예제 - 웹 MVC 개발

5-1. 회원 웹 기능 - 홈화면 추가

//홈 컨트롤러 추가
package hello.hellospring.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
   
@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>
  • 컨트롤러가 정적파일보다 우선순위가 높아서 index.html이 존재해도 homeController가 먼저 실행된다.

5-2. 회원 웹 기능 - 등록, 조회

//회원 등록 폼 컨트롤러
@Controller
public class MemberController {
   private final MemberService memberService;
  
   @Autowired
   public MemberController(MemberService memberService) {
       this.memberService = memberService;
   }
   
   //페이지 이동
   @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:/";
   }
   
   //조회
   @GetMapping(value = "/members")
   public String list(Model model) {
        List<Member> members = memberService.findMembers();
        model.addAttribute("members", members);
        return "members/memberList";
   }
   
}
<!-- 회원 등록 폼 html -->
<!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>
<!-- 회원 조회 -->
<!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>
//웹 등록 화면에서 데이터를 전달 받을 폼 객체
package hello.hellospring.controller;
public class MemberForm {
   private String name;
   public String getName() {
   	return name;
   }
   public void setName(String name) {
   	this.name = name;
   }
}

6. 스프링 DB 접근 기술

6-1. H2 데이터베이스 설치

6-2. 순수 JDBC

  • JdbcMemberRepository 작성
  • 저장소로 사용하고 있던 MemoryMemberRepository의 의존성을 제거하고 JdbcMemberRepository에 의존성 연결
  • 개발자가 직접 의존관계를 주입하게 된 경우에는 기존 소스코드를 복잡하게 수정해야 하지만 스프링의 특성인 DI를 사용하여 의존관계를 주입하게 된 경우에는 스프링의 설정파일만 변경하는 것으로 간단하게 의존관계를 변경할 수 있다.
//스프링 빈 설정파일
@Configuration
public class SpringConfig {
    private final DataSource dataSource;
    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);
    }
}

6-3. 스프링 통합 테스트

  • 스프링 컨테이너와 DB까지 연결한 통합 테스트 진행
  • @SpringBootTest : 스프링 컨테이너와 테스트를 함께 실행한다.
  • @Transactional : 테스트케이스에 이 애노테이션이 있으면 테스트 시작전에 트랜잭션을 시작하고 테스트 완료후에 항상 롤백한다. 이렇게 하면 데이터베이스에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다.
  • 단위테스트 : 순수 자바 코드로 이루어진 테스트
  • 통합테스트 : Spring이 포함되어 진행되는 테스트
  • 가장 좋은 테스트라고 말하기는 어렵지만 단위테스트로 쪼개어서 진행하는것이 바람직하다.

6-4. 스프링 JDBCTemplate

  • JDBCTemplate는 순수 JDBC의 반복코드는 대부분 제거해주지만 SQL은 직접 작성해야한다.
//JdbcMemberRepository 일부
@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;
    }

6-5. JPA

  • JPA는 기존의 반복코드제거와 기본적인 SQL작성이 가능하다.
  • JPA를 사용하면 SQL과 데이터 중심의 설계에서 객체 중심의 설계로 전환할 수 있으며, 개발 생산성을 크게 높일 수 있다.
//build.gradle파일에 JPA관련 라이브러리 추가
//해당 라이브러리는 내부에 JDBC관련 라이브러리 포함
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
//application.properties 파일에 설정 추가
//JPA가 생성하는 SQL문을 출력하는 설정
spring.jpa.show-sql=true
//JPA가 테이블을 자동으로 생성하는 기능 제거
spring.jpa.hibernate.ddl-auto=none
@Entity
public class Member {
     @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
     private Long id;
     
     private String name;
     
     public Long getId() {
    	 return id;
     }
     .
     .
     .
 }
  • 멤버 도메인에 엔티티 매핑을 하여 JPA가 사용할 수 있도록 한다.
//JpaMemberRepository.class
public class JdbcTemplateMemberRepository implements MemberRepository {
    private final JdbcTemplate jdbcTemplate;
    public JdbcTemplateMemberRepository(DataSource dataSource) {
        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(), id);
        return result.stream().findAny();
    }
    @Override
    public List<Member> findAll() {
        return jdbcTemplate.query("select * from member", memberRowMapper());
    }
    @Override
    public Optional<Member> findByName(String name) {
        List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper(), name);
        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;
        };
    }
}
@Transactional
public class MemberService {}
  • 스프링은 해당 클래스의 메소드를 실행할 때 트랜잭션을 시작하고, 메소드가 정상종료되면 트랜잭션을 커밋한다. 예외가 발생하면 롤백한다.
  • JPA를 통한 모든 데이터 변경을 트랜잭션안에서 실행해야한다.
//Spring.config 설정 변경
@Configuration
public class SpringConfig {

    private EntityManager em;

    public SpringConfig(EntityManager em){
        this.em = em;
    }

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository() {
        // return new MemoryMemberRepository();
        // return new JdbcMemberRepository(dataSource);
       // return new JdbcTemplateMemberRepository(dataSource);
        return new JpaMemberRepository(em);
    }
}

6-6. 스프링 데이터 JPA

public interface SpringDataJpaMemberRepository extends JpaRepository<Member,
Long>, MemberRepository {
 	Optional<Member> findByName(String name);
}
@Configuration
public class SpringConfig {

  private final MemberRepository memberRepository;
  
  public SpringConfig(MemberRepository memberRepository) {
  	 this.memberRepository = memberRepository;
   }
   
   @Bean
   public MemberService memberService() {
  	 return new MemberService(memberRepository);
   }
   
}
  • 스프링 데이터 JPA가 SpringDataJpaMemberRepository를 스프링 빈으로 자동 등록해준다.
  • JpaRepository에서 제공하는 메소드들을 통해 쿼리문 작성 없이 기본적인 CRUD가 가능하다.
  • findByName()과 같이 메소드 이름만으로 조회가 가능하다.
  • 페이징 기능도 자동으로 제공된다.

7. AOP

7-1. AOP가 필요한 상황

  • 🤔모든 메소드의 호출 시간을 측정하고 싶다면?
//회원가입 메소드에 시간측정로직 추가
@Transactional
public class MemberService {
   public Long join(Member member) {
   
       long start = System.currentTimeMillis();
       
       try {
           validateDuplicateMember(member); //중복 회원 검증
           memberRepository.save(member);
           return member.getId();
       } finally {
           long finish = System.currentTimeMillis();
           long timeMs = finish - start;
           System.out.println("join " + timeMs + "ms");
       }
    }
 }
  • 모든 메소드에 시간을 측정하는 코드를 작성해야한다.
  • 하지만 회원가입에서 시간을 측정하는 기능은 핵심 관심사항이 아니기 때문에 비즈니스 로직과 섞여서 유지보수도 어렵고, 시간측정로직을 변경할때 모든 로직을 찾아가면서 변경해야하는 문제점이 있다.

7-2. AOP 적용

  • AOP: Aspect Oriented Programming
  • 공통 관심 사항(cross-cutting concern) vs 핵심 관심 사항(core concern) 분리
@Component
@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");
        }
    }
}
  • 핵심 관심사항인 회원가입과 공통 관심사항인 시간측정 기능을 분리한다.
  • 핵심 관심사항을 깔끔하게 유지할 수 있다.
  • 공통 관심사항에 변경이 있으면 이 로직만 수정하면 되고, 공통 관심사항을 원하는 적용 대상을 선택할 수 있다.

0개의 댓글