[Spring] 의존성 주입 / 테스트 환경 구축 / Spring MyBatis / Mapper / Repository / Service

jngyoon·2023년 10월 10일
0

혼공일기

목록 보기
17/24

#230926 수업 복습

의존성 주입(Dependency Injection, DI)

외부에서 두 객체 간의 관계를 결정해주는 디자인 패턴
객체간 의존성을 개발자가 객체 내부에서 직접 호출하는 대신,
외부(스프링 컨테이너)에서 객체를 생성해서 넣어주는 방식

🔎 Chef, Hotel, Restaurant 예제

Hotel, Restaurant 객체 안에 Chef 객체를 주입

Chef.java

package com.kh.demo.sample;

import org.springframework.stereotype.Component;

@Component //@Component : 어떤 스프링의 요소라는 것을 어노테이션
public class Chef {
	
}

Hotel.java

package com.kh.demo.sample;

import org.springframework.stereotype.Component;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.ToString;

@ToString
@Getter
//@AllArgsConstructor //인스턴스 변수로 선언된 모든 것을 파라미터로 받는 생성자를 작성
//@RequiredArgsConstructor : 특정 변수를 위한 생성자를 만들 때 작성한다.
//						 	 final이나 @NonNull이 붙은 인스턴스 변수에 대한 생성자를 만들어낸다.
@RequiredArgsConstructor
@Component
public class Hotel {
	@NonNull //chef만 주입할 때
	private Chef chef;
	int data;
	
}

Restaurant.java

package com.kh.demo.sample;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import lombok.Data;
import lombok.Getter;
import lombok.Setter;

//@Data : @Getter, @Setter, @ToString, ... 을 하나로 합친 어노테이션
@Getter
@Component
public class Restaurant {
	//주입 방법 : 1. 생성자로 주입(@Data) 2. setter로 주입
	//setter가 호출될 때 @Autowired(주입)
	@Setter(onMethod_ = @Autowired) //롬복 사용시에는 언더바 붙여야함
	//@Autowired만 써도 됨 
	private Chef chef;
}
Restaurant 객체는 Chef 객체가 필요하다는 어노테이션(@Autowired) 설정이 있으므로,
등록되어 있던 Chef 객체의 레퍼런스를 Restaurant 객체에 주입한다.
=> Restaurant 객체가 Chef 객체를 사용하고 있는 경우, 
	Restaurant는 Chef에 의존성이 있다고 할 수 있음
=> Chef의 무언가가 변하면 그것이 Restaurant에 전달되어 영향을 미침.

의존성 주입 방법

1. 생성자 주입

@Autowired 
생성자에 @Autowired를 붙여 의존성을 주입 받을 수 있음

2. 필드 주입

필드에 바로 의존 관계를 주입하는 방법
외부에서 접근이 불가능하다는 단점이 있어 거의 사용되지 않음

3. setter(수정자) 주입

setter의 경우 객체가 변경될 필요성이 있을 때만 사용

테스트 환경(JUnit)

자바 프로그래밍 언어용 유닛 테스트 프레임워크
가장 많이 사용되는 테스트 환경
테스트 성공시 JUnit GUI 창에 녹색 / 적색으로 표시
하나하나의 케이스별로(단위로 나누어서) 테스트를 하는 단위 테스트 도구

테스트 환경 구축하기

1. 테스트 클래스 위쪽에 어노테이션 추가
	@SpringBootTest
2. 내부에 테스트용 메소드 선언
	내용은 우리가 테스트 해볼 로직으로 구현
	위쪽에 @Test 작성
3. 해당 클래스 JUnit Test로 실행

RestaurantTests.java

package com.kh.demo.sample;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class RestaurantTests {
	@Autowired
	private Restaurant restaurant;
	
	//@Test : JUnit에서 테스트 대상임을 표시
	@Test
	@DisplayName("테스트 이름")
	public void testExist() {
		//assert~~ : ~~이면 테스트 통과
//		assertNull(restaurant.getChef());  getChef()가 null이면 통과->null이 아니면 테스트 실패
		assertNotNull(restaurant.getChef());  //getChef()가 not null이면 통과
		System.out.println(restaurant);
	}
}

Spring MyBatis

SQL이 짧고 간결한 경우에는 어노테이션을 이용해서 쿼리문을 작성해준다.
SQL이 복잡하거나 길어지는 경우에는 어노테이션보다 XML을 이용하는 것이 좋다.
Mapper 인터페이스 위에는 @Mapper 를 작성해준다.

Spring-MyBatis의 경우 Mapper 인터페이스와 XML을 연동해서 동시에
이용할 수 있다. 
mapper인터페이스객체.메소드() 를 호출하는 순간
해당하는 인터페이스의 경로를 namespace로 가지고 있는 mapper XML 파일로 찾아가
메소드명과 동일한 id를 가지고 있는 쿼리문을 수행하고 결과로 돌려준다.

mapper 패키지 생성

src/main/java에 mapper패키지 생성 > Mapper 인터페이스 생성

TimeMapper.java

package com.kh.demo.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface TimeMapper {
	//Mapper 인터페이스
	//Mapper를 작성하는 작업은 XML을 이용할 수도 있지만,
	//최소한의 코드로 작성하기 위해서는 Mapper 인터페이스를 사용한다.
    
    //1. xml파일 없이 바로 연동
	@Select("select now() from dual")
	public String getTime1();
	
    //2. xml 파일을 이용하여 연동
	public String getTime2();
	
}
mapper 인터페이스를 만들고, @Mapper 어노테이션을 하면 바로 mapper로 만들어짐
=> 쿼리문 작성하는 xml 파일이 없어도 쿼리문을 돌릴 수 있음
ex) @Select("select 쿼리문")

TimeMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace는 mapper 인터페이스의 경로를 그대로 써준다. -->
<mapper namespace="com.kh.demo.mapper.TimeMapper">
	<!-- 'id 속성 값'은 쿼리를 사용할 때 호출할 '메소드의 이름'과 동일하게 맞추어야 한다. -->
	<select id="getTime2">
		select now() from dual
	</select>
</mapper>

mapper 테스트

MyBatis 관련 테스트는 스프링 전체 구동이 필요하지 않기 때문에
@MybatisTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
두 가지의 어노테이션을 작성해준다.

MapperTests.java

package com.kh.demo.persistence;

import org.junit.jupiter.api.Test;
import org.mybatis.spring.boot.test.autoconfigure.MybatisTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;

import com.kh.demo.mapper.TimeMapper;

@MybatisTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class MapperTests {
	@Autowired
	private TimeMapper mapper;

	@Test
	public void getTime1Test() {
		System.out.println(mapper.getTime1());
	}
	@Test
	public void getTime2Test() {
		System.out.println(mapper.getTime2());
	}
}

Repository (DAO 역할)

데이터 로직을 작성하는 곳
* 관리자 로그인, 회원 로그인, 회원가입 기능 세 가지가 있다고 가정

repository 인터페이스 생성

package com.kh.demo.repository;

public interface Repository {
	Object getObj(String id);
}
=> 이후에 UserRepository, AdminRepository에서
   Repository 인터페이스 상속 받아서 구현
   

UserRepository 인터페이스 생성

package com.kh.demo.repository;


public interface UserRepository extends Repository {
	int save(Object obj);
}
 

AdminRepository 인터페이스 생성

package com.kh.demo.repository;

public interface AdminRepository extends Repository{
	
}
=> Repository 상속 받아서 필요한 세부 repository 인터페이스를 생성하여 설계만 해 놓음(설계자 역할)

=> Impl 클래스에서 기능 구현(개발자 역할)   

AdminRepositoryImpl 클래스 생성

Impl 클래스에는 @Repository 어노테이션 작성
package com.kh.demo.repository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import com.kh.demo.mapper.AdminMapper;

@Repository
public class AdminRepositoryImpl implements AdminRepository{
	@Autowired
	private AdminMapper mapper;
	
	@Override
	public Object getObj(String id) {
		return mapper.get(id);
	}
}

UserRepositoryImpl 클래스 생성

package com.kh.demo.repository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import com.kh.demo.domain.UserDTO;
import com.kh.demo.mapper.UserMapper;

@Repository
public class UserRepositoryImpl implements UserRepository{
	@Autowired
	private UserMapper mapper;
	
	@Override
	public Object getObj(String id) {
		return mapper.get(id);
	}
	
	@Override
	public int save(Object obj) {
		return mapper.insert((UserDTO)obj);
	}
}

AdminDTO.java

package com.kh.demo.domain;

import lombok.Data;

@Data
public class AdminDTO {
	private String id;
	private String name;
	private int age;
}

UserDTO.java

package com.kh.demo.domain;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class UserDTO {
	private String id;
	private String name;
	private int age;
	private String regdate;
}

AdminMapper 인터페이스 생성

package com.kh.demo.mapper;

import org.apache.ibatis.annotations.Mapper;

import com.kh.demo.domain.AdminDTO;

@Mapper
public interface AdminMapper {
	AdminDTO get(String id);
}

UserMapper 인터페이스 생성

package com.kh.demo.mapper;

import org.apache.ibatis.annotations.Mapper;

import com.kh.demo.domain.UserDTO;

@Mapper
public interface UserMapper {
	int insert(UserDTO user);
	UserDTO get(String id);
}

AdminMapper.xml 생성

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace는 Mapper 인터페이스의 경로를 그대로 써준다. -->
<mapper namespace="com.kh.demo.mapper.AdminMapper">
	<select id="get" resultType="com.kh.demo.domain.AdminDTO">
		select * from admin where id=#{id}
	</select>
</mapper>

UserMapper.xml 생성

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace는 Mapper 인터페이스의 경로를 그대로 써준다. -->
<mapper namespace="com.kh.demo.mapper.UserMapper">
	<!-- id 속성 값은 쿼리를 사용할 때 호출할 메소드의 이름과 동일하게 맞추어야 한다. -->
	<insert id="insert">
		insert into user values(#{id},#{name},#{age},now())
	</insert>
	<select id="get" >
		select * from user where id=#{id}
	</select>
</mapper>

RepositoryTests.java 생성하여 테스트

package com.kh.demo.repository;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;

import com.kh.demo.domain.UserDTO;

@SpringBootTest
public class RepositoryTests {
	@Autowired @Qualifier("adminRepositoryImpl")	
    //@Autowired  뒤에 @Qualifier("주입할 클래스명")를 붙이면 여러 개의 동일한 타입의 빈이 존재할 때 어떤 빈을 주입해야하는지 지정할 수 있음
	private AdminRepository arep;
	@Autowired
	private UserRepository urep;
	
//	@Test
//	public void adminGet() {
//		System.out.println(arep.getObj("admin"));
//	}
	
	@Test
	public void userInsert() {
		UserDTO obj = UserDTO.builder()
				.id("apple")
				.name("김사과")
				.age(10)
				.build();
		System.out.println(urep.save(obj));		
	}
	
	@Test
	public void userGet() {
		System.out.println(urep.getObj("apple"));
	}
}

Service 패키지

Service는 비즈니스 로직 수행
데이터베이스에 접근하는 DAO를 이용해서 결과값을 받아옴

Service.java 생성

package com.kh.demo.service;

public interface Service {
	Object getDetail(String id);
	
}

AdminServiceImpl.java 생성

package com.kh.demo.service;

import org.springframework.beans.factory.annotation.Autowired;

import com.kh.demo.repository.AdminRepository;

public class AdminServiceImpl implements Service {
	@Autowired
	private AdminRepository repository;
	
	@Override
	public Object getDetail(String id) {
		return repository.getObj(id);
		
	}
}

0개의 댓글