티스토리 뷰

Web/Spring

Spring Boot MVC 특징

Aaron 2021. 4. 30. 21:36
반응형

기초부터 다시 차근차근 다져보자!

 

| 프로젝트 생성

프로젝트 생성은  Spring Initializr에서 (내 생각엔..) IDE보다 간편하게 만들 수 있다.

     - Project : Gradle Project

     - Language : Java 11

     - Packaging : Jar

     - Dependencies : Spring Web, Thymeleaf, devtools


|| Gradle 설정

Gradle 설정은 build.gradle 에서!

dependencies에 사용할 Library를 추가해주면 된다.

MVN Repository에서 의존성 주입할 수 있는 코드를 얻을 수 있다.

 


|| MVC 요소별 특징

어느정도 틀을 갖춘 프로젝트의 모습은 아래와 같다.

 

각 구조별로 특징(?)을 간략하게 살펴보자.

 

| Controller

기본 특징

/*
 * Spring이 실행되면서 Spring Container가 생기는데, 
 * @Controller annotation이 있다면 객체를 생성하여 Spring에 넣어 두고 관리
 * Spring Container에서 Spring Bean이 관리된다고 표현.
 */
@Controller
public class HelloController {

	/*
	 * Controller에서 매핑되는 URL이은 welcome page(static/index.html)보다 높은 우선순위를 갖음.
	 *
	 * 컨트롤러에서 리턴 값으로 문자를 반환하면 viewResolver가 화면을 찾아서 처리
	 * Spring Boot 템플릿 엔진은 기본으로 viewName 매핑 (resources:templates/{ViewName}.html)
	 */
	@GetMapping("hello")
	public String hello(Model model) {
		model.addAttribute("data", "hello!!");
		return "hello";
	}

	@GetMapping("hello-mvc")
	public String helloMvc(@RequestParam("name") String name, Model model) {
		model.addAttribute("name", name);
		return "hello-template";
	}

	/*
	 * @ResponseBody를 사용하면 viewResolver를 사용하지 않음
	 * 객체를 반환하면 객체가 JSON으로 변환 (HttpMessageConverter 동작)
	 */
	@GetMapping("hello-string")
	@ResponseBody
	public String helloString(@RequestParam("name") String name) {
		return "hello " + name;
	}

	@GetMapping("hello-api")
	@ResponseBody
	public Hello helloApi(@RequestParam("name") String name) {
		Hello hello = new Hello();
		hello.setName(name);
		return hello;
	}

	static class Hello {
		private String name;

		public String getName() {
			return name;
		}

		public void setName(String name) {
			this.name = name;
		}
	}
}

 

 

Controller에서 DI를 주입하는 3가지 방법

@Controller
public class MemberController {

	// 1. DI 필드 주입
	/*
	 * 단점 : Spring이 가동될 때를 제외하고 주입 대상을 바꿀 수 있는 방법이 없어서 안 좋음!!
	 */
	 @Autowired private MemberService memberService;

	// 2. DI 생성자 주입 (요즘 추천하는 방법)
	/*
	 * Spring 조립 시 설정해두고 의존관계가 더이상 바꿀 수 없도록 설정
	 *
	 * DI (Depedency Injection) : 의존 관계를 주입! Member Controller가 생성이 될 때 Spring Bean에
	 * 등록되어 있는 memberService 객체를 사용
	 */
	private final MemberService memberService;

	@Autowired
	public MemberController(MemberService memberService) {
		this.memberService = memberService;
	}

	// 3. DI Setter 주입
	/*
	 * 단점 : Spring 조립 후 중간에 setter를 호출할 일이 없는데, pubilc 메서드로 노출되어서 좋지 않음!!
	 */
	private MemberService memberService;
	
	@Autowired
	public void setMemberService(MemberService memberService) {
		this.memberService = memberService;
	}
	
    // ..
}

 

| Domain

객체 Class라고 생각하면 편하다.

@Entity
public class Member {

	@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	@Column(name = "name")
	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;
	}
}

 

| Repository

JdbcTemplate를 사용할 경우

public class JdbcTemplateMemberRepository implements MemberRepository {

	private final JdbcTemplate jdbcTemplate;

	// 생성자가 딱 하나만 있다면 @Autowired 생략 가능
	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;
		};
	}
}

 

 

JPA를 사용할 경우

public class JpaMemberRepository implements MemberRepository {

	// JPA는 EntityManager로 모든 것이 동작
	private final EntityManager em;

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

	public Member save(Member member) {
		em.persist(member);
		return member;
	}

	public Optional<Member> findById(Long id) {
		Member member = em.find(Member.class, id);
		return Optional.ofNullable(member);
	}

	public List<Member> findAll() {
		return em.createQuery("select m from Member m", Member.class).getResultList();
	}

	public Optional<Member> findByName(String name) {
		List<Member> result = em.createQuery("select m from Member m where m.name = :name", Member.class) // JPQL
				.setParameter("name", name).getResultList();

		return result.stream().findAny();
	}
}

 

 

Spring Data JPA를 사용할 경우

											    					// Type, ID
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
	
	/**
	 * Spring Data JPA가 JpaRepository Interface를 상속받고 있으면 구현체를 자동으로 만들어준다.
	 * 그리고 Spring Bean에 자동으로 등록해준다.
	 */
	
	// JPQL select m from Member m where m.name = ?
	@Override
	Optional<Member> findByName(String name);
	
}

 

 

| Service

기본 특징

/*
 * Spring Container에 Service 객체로 등록
 * 
 * @Transactional : JPA는 join 들어올 때 모든 데이터 변경이 transaction 안에서 실행되어야 함
 */
@Transactional
public class MemberService {
	
	private final MemberRepository memberRepository;
	
	// DI (Depedency Injection) : 외부에서 Repository를 주입
	// MemberService는 memberRepository를 의존 
	public MemberService(MemberRepository memberRepository) {
		this.memberRepository = memberRepository;
	}

	/**
	 * 회원가입
	 */
	public Long join(Member member) {
		validateDuplicateMember(member); // 중복 회원 검증
		memberRepository.save(member);
		return member.getId();
	}

	private void validateDuplicateMember(Member member) {
		memberRepository.findByName(member.getName()).ifPresent(m -> {
			throw new IllegalStateException("이미 존재하는 회원입니다.");
		});
	}

	/**
	 * 전체 회원 조회
	 */
	public List<Member> findMembers() {
		return memberRepository.findAll();
	}

	public Optional<Member> findOne(Long memberId) {
		return memberRepository.findById(memberId);
	}
}

 

| SpringApplication

/*
 * @SpringBootApplication의 패키지 포함 하위 패키지를 모두 탐색(Component Scan)해서 Spring Bean으로 등록
 * 
 * 참고 : Spring Container에 Spring Bean으로 등록할 때, 기본으로 싱클톤 등록.
 * 유일하게 하나만 등록하여 공유하므로 같은 Spring Bean이면 모두 같은 인스턴스!
 */
@SpringBootApplication
public class HelloSpringApplication {

	public static void main(String[] args) {
		SpringApplication.run(HelloSpringApplication.class, args);
	}

}

 

| Config

/*
 * Spring이 실행되면 @Configuration을 읽고, 그 안에 @Bean들을 Spring에 등록 
 */
@Configuration
public class SpringConfig {

	private final MemberRepository memberRepository;
	
	/*
	 *  @Autowired가 있으면 스프링이 연관된 객체를 스프링 컨테이너에서 찾아서 넣어준다
	 *  객체 의존관계를 외부에서 넣어주는 것을 DI
	 */
	@Autowired
	public SpringConfig(MemberRepository memberRepository) {
		/**
		 * Spring Data JPA가 Spring Bean에 등록해놓은 객체를 가져다 쓰면 끝!
		 */
		this.memberRepository = memberRepository;
	}

	@Bean
	public MemberService memberService() {
		return new MemberService(memberRepository);
	}
	
	@Bean
	public TimeTraceAop timeTraceAop() { // 직접 Bean으로 등록하면서 아~ AOP가 쓰이는구나를 인지
		return new TimeTraceAop();
	}

//	@Bean
//	public MemberRepository memberRepository() {
		// Spring DI를 사용하면 기존 코드를 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있음
//		return new MemoryMemberRepository();
//		return new JdbcMemberRepository(dataSource);
//		return new JdbcTemplateMemberRepository(dataSource);
//		return new JpaMemberRepository(em);
//	}
}

 

| AOP

/**
 * @Aspect : AOP임을 명시, 핵심 관심사항(추가, 조회 등)과 공통 관심 사항(시간 측정)을 분리
 * @Component : ComponentScan으로 Spring Bean 등록을 할 수 있지만 직접 등록해서 사용하는 것을 선호
 * 				AOP는 특별하니까!
 */
@Aspect
public class TimeTraceAop {
	
	@Around("execution(* hello.hellospring..*(..))") // AOP 적용 범위
	public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
		
		long start = System.currentTimeMillis();
		System.out.println("START: " + joinPoint.toString()); // 어떤 메서드가 Call 되었는지
		
		try {
			return joinPoint.proceed(); // 다음 메서드로 진행
		} finally {
			long finish = System.currentTimeMillis();
			long timeMs = finish - start;
			System.out.println("END: " + joinPoint.toString() + " " + timeMs + "ms");
		}
	}
}

|| TEST

| 단위 테스트

/*
 * 단위 테스트
 * 순수한 단위 테스트가 훨씬 좋은 테스트일 가능성이 높다!
 */
class MemberServiceTest {
	
	MemberService memberService;
	MemoryMemberRepository memberRepository;

	// 각 테스트 실행 전에 호출
	// 테스트가 서로 영향이 없도록 항상 새로운 객체를 생성하고, 의존관계도 새로 맺어줌
	@BeforeEach
	public void beforeEach() {
		memberRepository = new MemoryMemberRepository();
		memberService = new MemberService(memberRepository);
	}

	// 메서드 실행이 끝날 때마다 동작
	// 테스트는 서로 의존관계 없이 동작해야 한다!
	@AfterEach
	public void afterEach() {
		memberRepository.clearStore();
	}

	@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("이미 존재하는 회원입니다.");
		/*
		try {
			memberService.join(member2);
			fail();
		} catch (IllegalStateException e) {
			assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
		}
		*/
	}
}

 

 

| 통합 테스트

/*
 * Spring 통합 테스트
 * 
 * @SpringBootTest : 스프링 컨테이너와 테스트를 함께 실행
 * @Transactional : 테스트 케이스에 이 애노테이션이 있으면, 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 롤백 
 *   				(각 테스트마다 적용)   
 */
@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("이미 존재하는 회원입니다.");
	}
}

 

Reference : 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술
반응형
댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday