티스토리 뷰
반응형
기초부터 다시 차근차근 다져보자!
| 프로젝트 생성
프로젝트 생성은 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 접근 기술
반응형
'Web > Spring' 카테고리의 다른 글
[SpringBoot] Security Login/Logout Page (0) | 2021.05.15 |
---|---|
jsp를 thymeleaf로 변환하기 (convert jsp to thymeleaf) (0) | 2021.05.09 |
Github에 있는 SpringBoot(maven, gradle) Project Repository import하기(STS, Eclipse) (3) | 2021.04.26 |
Local에 있는 SpringBoot(maven, gradle) project import하기(STS, Eclipse) (0) | 2021.04.26 |
[Spring] File Upload, Download (파일 업로드, 다운로드 기능 만들기) (4) | 2020.12.02 |
댓글