Sring Security 를 이용하여 JWT를 인증하는 방법이다.

기본적으로 DB연결이 설정 되어 있다는 것과 일반적인 Spring Security Form Login을 알고 있다는 가정하에 시작한다. 되어 있지 않다면 아래의 글을 참고한다.

2021/01/19 - [Develop/Spring Boot] - Spring Boot Security form login

 

Spring Boot Security form login

Spring Boot 에서 Security Form Login 적용 방법이다. 먼저, 간단한 DB, 의존성 세팅과 DB연결 후 Security를 적용한다. 1. 기본 세팅 1.1 DB설정 로그인에 사용할 DB테이블을 먼저 생성한다. CREATE TABLE `user..

wky.kr

 

1. SecurityConfig 생성

SecurityConfig.java 파일을 생성하여 WebSecurityConfigurerAdapter를 상속받고, 아래 3개의 메소드를 Override한다.

 

1. configure(HttpSecurity http) 메소드에서는 Jwt 인증이 실패할 경우 처리할 EntryPoint 설정과 JWT인증에는 기본적으로 세션을 사용하지 않기 때문에 Session을 STATELESS해준다. 또한, JWT인증을 처리할 Filter를 SpringSecurity의 기본적인 필터인 UsernamePasswordAuthenticationFilter 앞에 넣어준다.

 

2. configure(AuthenticationManagerBuilder auth) 메소드는 인증방법을 의미하며,

Spring에서 xml로 Security 설정시 <authentication-manager> <authentication-provider>를 설정하는 것과 같다. 예를 들면 DB에서 사용자 정보를 불러와 현재 입력된 정보와 비교 후 UserDetails 을 반환하여 인증하겠다 라는 의미이다.
현재는 CustomUserDetailSerivce 객체에 인증 방법이 있으며, 패스워드 인코더를 설정해 두었다.

 

3. PasswordEncoder는 로그인시 DB에 BCrpyt로 저장되어있는 패스워드를 로그인할 때 입력한 비밀번호와 비교하기 위해 사용되며, 이는 인증할 때 사용되는 configure(AuthenticationManagerBuilder auth) 메소드에 사용될 것이다.

 

4. AuthenticationManger는 이 글의 뒤에서 나중에 진짜로 인증을 하는 곳에서 사용하기 위해 Bean으로 등록한다.

 

CustomUserDetailService는 이전 글인 Spring Boot Security form login 에서 볼 수 있다.

 

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	private JwtAuthenticationFilter jwtAuthenticationFilter;
	
	@Autowired
	private CustomUserDetailService userDetailsService;
	
	@Autowired
	private JwtEntryPoint jwtPoint;
	
	@Bean
	@Override
	public AuthenticationManager authenticationManagerBean() throws Exception {
		return super.authenticationManagerBean();
	}
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// TODO Auto-generated method stub
		http
			.csrf().disable()
			.authorizeRequests()
				.antMatchers("/login").permitAll()
				.anyRequest().authenticated()
			.and()
			.exceptionHandling()
			.authenticationEntryPoint(jwtPoint)
			.and()
			.sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
			.and()
			.addFilterBefore(
					jwtAuthenticationFilter,
		            UsernamePasswordAuthenticationFilter.class
		        );
	}
	
    @Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		// TODO Auto-generated method stub
    	auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
	}

	@Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

 

2. JWT 인증 Filter 생성

요청이 들어올 때 마다 JWT를 인증할 Filter를 생성한다. 요청이 들어올 때마다 하기 위해 OncePerRequestFilter를 상속받는다. OncePerRequestFilter가 아니더라도 일반적인 GenericFilterBean을 상속받아도 무관하다. (OncePerRequestFilter가 이미 GenericFilterBean를 상속받고 있기 때문.)

 

Filter 작동 방식은 간단하다. Bearer로 토큰을 받으면 토큰을 추출하여 올바른 토큰인지 체크를 한다.

토큰이 올바르다면, 토큰에 있는 정보(username, role 등등) 을 가져와 인증을 시킨다.

 

토큰이 없거나, 올바르지않다면 그냥 다음으로 넘어간다.

 

CustomUserDetailService는 이전 글인 Spring Boot Security form login 에서 볼 수 있다.

 

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

	@Autowired
	private JwtProvider jwtProvider;

	@Autowired
	private CustomUserDetailService userDetailsService;

	private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		try {	
			String jwt = getToken(request);
			if (token != null && jwtProvider.validateJwtToken(token)) {
				String username = jwtProvider.getUserNameFromJwtToken(token);

				UserDetails userDetails = userDetailsService.loadUserByUsername(username);
				UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
						userDetails, null, userDetails.getAuthorities());
				authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

				SecurityContextHolder.getContext().setAuthentication(authentication);
			}
		} catch (Exception e) {
			logger.error("Cannot set user authentication: {}", e);
		}

		filterChain.doFilter(request, response);
	}

	private String getToken(HttpServletRequest request){
		String headerAuth = request.getHeader("Authorization");

		if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
			return headerAuth.substring(7, headerAuth.length());
		}

		return null;
	}
}

 

3. JWT Provider 생성

토큰을 인증하거나 생성할 Provider를 생성한다.

generateJwtToken 메소드로 로그인 후 인증에 성공시 토큰을 생성한다.

validateJwtToken 메소드로 토큰이 올바른지 검사한다. 필터에서 이 메소드를 통해 토큰 인증을 진행하고 인증이 성공하면 다음단계로 넘어가고 인증실패할 경우 에러를 호출한다.

getUserNameFromJwtToken 으로 토큰에 포함되어 있는 Body를 추출할 수 있다.

 

@Component
public class JwtProvider {
	
	private static String jwtSecret ="www.wky.krSecretKey";
	private static final Logger logger = LoggerFactory.getLogger(JwtProvider.class);

	private static int jwtExpirationMs =86400000;

	public String generateJwtToken(Authentication authentication) {
		String name = authentication.getName();
		
		return Jwts.builder()
				.setSubject(name)
				.setIssuedAt(new Date())
				.setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
				.signWith(SignatureAlgorithm.HS512, jwtSecret)
				.compact();
	}

	public String getUserNameFromJwtToken(String token) {
		return Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody().getSubject();
	}

	public boolean validateJwtToken(String authToken) {
		try {
			Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
			return true;
		} catch (SignatureException e) {
			logger.error("Invalid JWT signature: {}", e.getMessage());
		} catch (MalformedJwtException e) {
			logger.error("Invalid JWT token: {}", e.getMessage());
		} catch (ExpiredJwtException e) {
			logger.error("JWT token is expired: {}", e.getMessage());
		} catch (UnsupportedJwtException e) {
			logger.error("JWT token is unsupported: {}", e.getMessage());
		} catch (IllegalArgumentException e) {
			logger.error("JWT claims string is empty: {}", e.getMessage());
		}

		return false;
	}
}

 

4. EntryPoint 생성

SecurityConfig에서 등록했던 인증에 실패할 경우 진행될 EntryPoint를 생성한다.

AuthenticationEntryPoint를 implements하여 commence를 작성한다 commence에는 인증이 실패할 경우 넘어갈 URL을 호출하거나 에러메세지를 호출한다.

 

@Component
public class JwtEntryPoint implements AuthenticationEntryPoint {

	private static final Logger logger = LoggerFactory.getLogger(JwtEntryPoint.class);
	@Override
	public void commence(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException authException) throws IOException, ServletException {
		// TODO Auto-generated method stub
		logger.error("Unauthorized error: {}", authException.getMessage());
		response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized");
	}
}

 

5. Login 처리

Login을 직접 처리할 Controller를 생성한다.

LoginVO에는 userId, userPw, token 변수가 선언되어 있다.

프론트에서 userId와 userPw를 JSON으로 전달받아 SecurityConfig에서 Bean으로 등록했던

authenticationManager를 통해 인증을 시도한다. 여기서 인증이 성공되면 다음 코드로 넘어가고

실패하면 SecurityConfig에서 등록한 EntryPoint가 실행된다.

 

성공 후 인증한 객체정보를 가지고 토큰을 생성하고 Token을 반환한다.

반환 객체는 필요한 데이터에 맞게 알맞게 생성하여 반환하면 Token과 함께 반환만 하면 된다.

 

	@PostMapping("/login")
	@ResponseBody
	public ResponseEntity<LoginVO> login(HttpServletResponse response, @RequestBody LoginVO loginVO) {

		Authentication authentication = authenticationManager
				.authenticate(new UsernamePasswordAuthenticationToken(loginVO.getUserId(), loginVO.getUserPw()));

		// SecurityContextHolder.getContext().setAuthentication(authentication);
		String jwt = jwtProvider.generateJwtToken(authentication);
		loginVO.setUserPw("");
		loginVO.setToken(jwt);
		return ResponseEntity.ok(loginVO);

	}

 

 

이러한 작동방식으로 Front에선 Token을 받아 LocalStroage에 Bearer + Token 형태로 저장 후

백엔드로 데이터를 요청할때 LoacalStorage에서 저장한 Token을 꺼내어  Authorization 헤더에 저장한 후 요청한다.

Header : Authorizaion Bearer token

 

번 외

LocalStorage에 저장하여 Header 에 계속 Token을 세팅하기 불편하고 Cookie로 하고싶을 땐 아래와 같이 사용하면 된다.

 

번외 1. Login처리

위에서 사용한 Login처리 Controller에서 Token 생성후 아래와 같이 Cookie에 적용한다.

 

	@PostMapping("/login")
	@ResponseBody
	public ResponseEntity<LoginVO> login(HttpServletResponse response, @RequestBody LoginVO loginVO) {

		Authentication authentication = authenticationManager
				.authenticate(new UsernamePasswordAuthenticationToken(loginVO.getUserId(), loginVO.getUserPw()));

		//SecurityContextHolder.getContext().setAuthentication(authentication);
		String jwt = jwtProvider.generateJwtToken(authentication);
		Cookie cookie = new Cookie("token", "Bearer " + jwt);
		
		cookie.setPath("/");
		cookie.setMaxAge(60 * 60 * 24 * 7);
		response.addCookie(cookie);

		loginVO.setUserPw("");
		loginVO.setToken(jwt);
		return ResponseEntity.ok(loginVO);

	}

 

번외 2. JWT 인증 Filter 생성

위에서 생성한 Filter에 아래와 같이 Cookie에 있는 정보를 가져오는 코드를 작성하여 Cookie에 있는 token을 가져온다.

가져온 뒤의 절차는 위와 동일하다.

 

@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		try {
			String token = Arrays.stream(request.getCookies())
            .filter(c -> c.getName().equals("token"))
            .findFirst()
            .map(Cookie::getValue)
            .orElse(null);
			
			String jwt = getToken(request);
			if (jwt != null && jwtProvider.validateJwtToken(jwt)) {
				String username = jwtProvider.getUserNameFromJwtToken(token);

				UserDetails userDetails = userDetailsService.loadUserByUsername(username);
				UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
						userDetails, null, userDetails.getAuthorities());
				authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

				SecurityContextHolder.getContext().setAuthentication(authentication);
			}
		} catch (Exception e) {
			logger.error("Cannot set user authentication: {}", e);
		}

		filterChain.doFilter(request, response);
	}

	private String getToken(String token){
		String headerAuth = token;

		if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
			return headerAuth.substring(7, headerAuth.length());
		}

		return null;
	}

 

이러한 방식으로 JWT인증을 LocalStorage 뿐만아니라 Cookie를 이용하여 작성할 수 있다.

 

참고 : bezkoder.com/spring-boot-jwt-authentication/

 

Spring Boot Token based Authentication with Spring Security & JWT - BezKoder

Spring Boot JWT Authentication example with MySQL/PostgreSQL and Spring Security - Spring Boot 2 Application with Spring Security and JWT Authentication

bezkoder.com

 

Spring Boot 에서 Security Form Login 적용 방법이다.

먼저, 간단한 DB, 의존성 세팅과 DB연결 후 Security를 적용한다.

 

1. 기본 세팅

1.1 DB설정

로그인에 사용할 DB테이블을 먼저 생성한다. 

CREATE TABLE `user` (
	`id` VARCHAR(50) NOT NULL,
	`pw` VARCHAR(255) NULL DEFAULT NULL,
	PRIMARY KEY (`id`)
)

 

1.2 dependency 설정

DB는 현재 MariaDB와 JPA를 사용하며, spring security 의존성과 함께 추가한다.

implementation 'mysql:mysql-connector-java'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'

 

1.3 DB 연결 설정

application.yml(properties) 파일에 DB 연결 정보를 추가 한다.

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: "jdbc:mysql://localhost:3306/test?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Seoul"
    username:
    password:
  jpa:
    hibernate:
      ddl-auto: update
    generate-ddl: false
    show-sql: true
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
    properties: 
      hibernate:
        format_sql: true

 

2. JPA 적용

Jpa는 Entity와 Interface로 이루어진 Repository를 생성하면 간단하게 적용할 수 있다.

2.1 Entity 생성

UserVO를 생성하여 생성한 DB테이블에 맞게 VO에 Entity와 Column명 들을 설정한다.

@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "user")
public class UserVO {

	@Id
	@Column(name = "id")
	private String id;
	
	@Column(name = "pw")
	private String pw;
	
	@Column(name = "name")
	private String name;
}

 

2.2 JpaRepository 생성

VO로 생성한 table을 연결할 JpaRepository를 생성한다.

public interface UserRepository extends JpaRepository<UserVO, String> {
	
}

 

3. Security 적용

3.1 WebSecurityConfigurerAdapter 적용

먼저 WebSecurityConfigurerAdapter를 상속받고, 아래 2개의 메소드를 Override하고 Bean을 등록한다.

 

1. configure(HttpSecurity http) 메소드는 기본적인 security 기본 설정을 하는 것이다. 현재는 csrf를 비활성화 시키고,

authorizeRequests()를 이용하여 요청에대한 권한을 처리한다. /login 경로는 권한이 없어도 접속을 허용하고, anyRequest().authenticated()를 통해 이 외의 경로는 권한이 있어야 접속을 허용한다.

formLogin() 을 통해 formLogin을 사용하겠다는 것이며, formLogin 페이지를 만들지 않았다면, Security에서 제공해주는 로그인페이지를 사용한다. 만약 로그인페이지가 있다면. formLogin().loginPage("로그인페이지 경로") 라는 loginPage 옵션을 추가하여 로그인페이지를 커스텀페이지로 변경할 수 있다.

formLogin에는 로그인 성공시 작동할 기능, 실패시 작동할 기능, 로그인 인증 URL 변경 등 다양한 기능들이 있다.

 

2. configure(AuthenticationManagerBuilder auth) 메소드는 인증방법을 의미하며,

Spring에서 xml로 Security 설정시 <authentication-manager> <authentication-provider>를 설정하는 것과 같다. 예를 들면 DB에서 사용자 정보를 불러와 현재 입력된 정보와 비교 후 UserDetails 을 반환하여 인증하겠다 라는 의미이다.
현재는 CustomUserDetailSerivce 객체에 인증 방법이 있으며, 패스워드 인코더를 설정해 두었다.

 

3. PasswordEncoder는 로그인시 DB에 BCrpyt로 저장되어있는 패스워드를 로그인할 때 입력한 비밀번호와 비교하기 위해 사용되며, 이는 인증할 때 사용되는 configure(AuthenticationManagerBuilder auth) 메소드에 사용될 것이다.

 

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	private CustomUserDetailService userDetailsService;
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// TODO Auto-generated method stub
		http
			.csrf().disable()
			.authorizeRequests()
				.antMatchers("/login").permitAll()
				.anyRequest().authenticated()
			.and()
			.formLogin();
	}
	
    @Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		// TODO Auto-generated method stub
    	auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
	}

	@Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

 

3.2 인증을 처리할 메소드

아이디와 비밀번호를 통해 인증을 직접적으로 처리할 메소드이다.

UserDetailsService를 implements하여 loadUserByUsername을 Override한다.

권한을 임의로 생성하였으며,

2번에서 생성한 Repository를 통해 userRepository.findById(로그인아이디)를 사용하여, DB에 저장되어있는 아이디와 비밀번호를 불러온다.

불러온후 security.core에서 제공해주는 User객체를 생성하여 반환한다.

 

configure(AuthenticationManagerBuilder auth)에서 설정한 BCrpyt PasswordEncoder에는 macth라는 패스워드 비교하는 기능이 있는데 User 객체를 반환하면서 현재의 비밀번호와 폼에서 입력한 비밀번호가 match 기능으로 인하여 비밀번호를 비교해주고 비밀번호가 올바르면 로그인 인증 성공으로 처리하고 틀리다면 실패로 처리한다.

import org.springframework.security.core.userdetails.User;

@Service
public class CustomUserDetailService implements UserDetailsService {

	@Autowired
	private UserRepository userRepository;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		// TODO Auto-generated method stub
		
        Collection<SimpleGrantedAuthority> roles = new ArrayList<SimpleGrantedAuthority>();
		roles.add(new SimpleGrantedAuthority("ROLE_USER"));
		
        UserVO userVO =userRepository.findById(username).orElseThrow(() -> new NoSuchElementException());
		return new User(userVO.getId(), userVO.getPw(), roles);
		
	}

}

 

 

이러한 방법을 이용하여, 생성한 DB User테이블에 임의의 ID와 BCrpyt로 암호화한 패스워드를 넣고,

서버를 실행하여 /login 경로로 접속하면, Spring Security에서 기본으로 제공하는 Login Form을 이용하여 로그인을 할 수 있다.

 

이전 글에 이어서 Kotlin + Spring Boot 2.0 + Webflux + R2DBC(MySQL)사용방법이다.

 

이전글 - Kotlin Spring Boot Webflux (Eclipse)

2020/04/26 - [Develop/Spring Boot] - Kotlin Spring Boot Webflux (Eclipse)
 

Kotlin Spring Boot Webflux (Eclipse)

이클립스에서 Kotlin + Spring Boot 2.0 + Webflux 사용방법이다. DB연동은 다음에 하며 간단한 프로젝트 생성 후 Get, Post, Put, Delete 부터시작한다. 1. https://start.spring.io/ 불러오는 중입니다... 접속..

wky.kr

 

0. DB 테이블 생성

프로젝트에 사용할 MySQL 테이블을 생성한다

create table test{
	id int(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
    name varchar(50)
}

 

1. 이전 프로젝트 수정

1.1 TestHandler에 getAllTest 메소드를 추가한다.

 

TestHandler.kt

 

1.2 router패키지의 index를 아래와 같이 변경한다.

index.kt

 

1.3 model의 Test.kt 수정

Test.kt 에서 String으로 선언한 id를 Int형으로 수정한다.

Test.kt

 

2. DB 연결(R2DBC)

2.1 Maven 추가

R2DBC Start Maven과 MySQL Maven을 추가한다.

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>

<dependency>
	<groupId>dev.miku</groupId>
	<artifactId>r2dbc-mysql</artifactId>
</dependency>

 

2.2 DB 연결

2.2.1 Config패키지와 R2DBC를 연결할 코틀린파일을 생성한다.

com.example.demo.config 패키지, R2DBCConfig.kt 파일생성

 

 

2.2.2 DB Connection 설정

생성한 R2DBCConfig파일에 AbstractR2dbcConfiguration을 상속받고 아래와 같이 작성한다.

host : DB 접속 URL (ip주소를 입력할 땐 앞에 http를 생략해야한다. http를 붙일 경우 connection error 발생)

password : DB 비밀번호

port : DB 포트

database : DB 이름

username : DB 계정

 

R2DBCConfig.kt

 

2.2.3 @EnableR2dbcRepositories

메인 코틀린 파일에 @EnableR2dbcRepositories 어노테이션을 붙여준다.

DemoApplication.kt

 

3. DAO생성 및 Model 수정

3.1 DAO 패키지와 인터페이스를 생성한다.

TestDAO.kt

 

3.2 DAO 설정

TestDAO에 ReactiveCrudRepository를 추가하고 Model을 연결한다.

TestDAO.kt

 

3.3 Model 수정

Test.kt 파일을 수정한다. DB 연결에 필요한 annotation 및 생성자를 추가한다.

아래의 생성자는 id를 통해 DB의 데이터를 삭제하기 위해 생성하였다.

Test.kt

 

4. Handler 수정

4.1 TestDAO 선언

TestHandler.kt에 생성한 TestDAO를 선언한다.

TestHandler.kt

 

4.2 생성한 Handler Method들 수정

4.2.1 소스 수정

TestHandler.kt

 

 

4.2.2 PostMan으로 테스트

추가

 

전체 조회 :

 

수정 : id 2의 name을 test222로 수정

 

하나만 조회 : 수정한 id : 2 인 데이터가 수정이 잘되었는지 조회

 

삭제 :

 

5. 기타

현재 테스트한 테이블에서는 id가 Auto Increment로 되어있다. 따라서 save 메소드 사용할 경우 insert와 update가 잘 되지만,

만약 auto increment가 아닌 테이블에 데이터를 삽입하게 된다면, save를 사용할 경우 insert가 되지않고 update문이 적용될 것이다.

이를 해결하기 위해선 Persistable이라는 interface를 상속받아 isNew의 반환값을 true로 하면 해결이되나, 이는 또 다시 insert만되고 update는 안되는 반대되는 상황이 발생한다. 

아직 ReactiveCrudRepository에서는 정확한 해결방법이 나오진 않은 것 같다.

 

다음 - R2DBC Join, Query사용 Dynamic Projection

1. Maven Archetype

Archetype이란 Maven(Spring, Spring boot)에서 나만의 template/boilerplate를 제작하는 방법이다.

 

2. 기본세팅

2.1 Setting.xml

- Archetype 사용하기 위해선 setting.xml 파일이 필요하다.

- setting.xml 파일이 없다면, 내문서의 .m2 폴더에 setting.xml 파일을 생성한다.

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
                      http://maven.apache.org/xsd/settings-1.0.0.xsd">


  <localRepository>${user.home}/.m2/repository</localRepository>

</settings>

 

2.2 Maven

https://maven.apache.org/download.cgi

 

Maven – Download Apache Maven

Downloading Apache Maven 3.6.3 Apache Maven 3.6.3 is the latest release and recommended version for all users. The currently selected download mirror is http://us.mirrors.quenda.co/apache/. If you encounter a problem with this mirror, please select another

maven.apache.org

- Archetype은 기본적으로 Maven명령어를 사용하기 때문에 Maven이 설치되어 있어야 한다.

- 위 사이트에 접속하여 .zip 파일을 다운받아 적절한 위치에 압축을 해제한 뒤 고급시스템 설정의 path에

압축 해제한 경로에 맞게(Ex : C:\apache-maven-3.6.3\bin) path를 추가한다.

 

※ Spring/Spring Boot는 Maven 설치안하는데 Maven이 되는데, 그 이유는 Spring/Spring Boot의 경우 프로젝트를 생성하게 되면, 자동으로 mvnw.cmd 라는 파일이 생기기 때문이다.

 

3. Archetype 사용방법

3.1 미리 생성한 프로젝트에서 생성 후 로컬배포

- Spring/Sprin Boot에서 나만의 template/boilerplate를 제작하였다면, IDE Terminal에서 프로젝트 Root 경로에 아래의 명령어를 입력한다

mvn archetype:create-from-project

 

- 명령어를 입력하게되면 target/generated-sources/archetype 폴더가 생성된다. 생성한 폴더에 접근한다.

cd target/generated-soruces/archetype

 

- IDE Terminal에서 mvn install 또는 mvn deploy를 실행한다.

mvn install

  - mvn install은 local 저장소에 배포를 하며, .m2 폴더에 저장된다.

  - mvn deploy는 maven 저장소에 배포하는 것이며, 사설 nexus 저장소에 배포가능하다.

 

 

- mvn archetype:generate -DarchetypeCatalog=local 명령어를 통해 정상적으로 등록되었는지 확인한다.

mvn archetype:generate -DarchetypeCatalog=local

 

- mvn install 했으나 로컬에 배포되지 않은 경우 mvn archetype:update-locate-catalog 를 입력한다.

mvn archetype:update-locate-catalog

 

3.2 사설 저장소에 배포

- mvn deploy로 사설 nexus에 등록할 경우 target/generated-soruces/archetype 에 있는 pom.xml에 아래와 같이 등록한다. 

	<distributionManagement>
        <repository>
            <id>test-releases</id> 
            <name>Releases Repository</name>
            <url>http://127.0.0.1/repository/maven-releases/</url>
        </repository>
        <snapshotRepository>
            <id>test-snapshots</id>
            <name>Snapshots Repository</name>
            <url>http://127.0.0.1/repository/maven-snapshots/</url> 
        </snapshotRepository>
    </distributionManagement>

 

- 만약 사설 저장소에 권한이 필요해 계정 및 비밀번호가 필요하면, .m2 폴더의 settings.xml에 계정과 비밀번호를 등록한다. server에 들어가는 id 와 위 xml의 id가 같아야한다.

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
                      http://maven.apache.org/xsd/settings-1.0.0.xsd">


  <localRepository>${user.home}/.m2/repository</localRepository>

  <servers>
    <server>
      <id>test-releases</id>
      <username>admin</username>
      <password>testpw</password>
    </server>
     <server>
      <id>test-snapshots</id>
      <username>admin</username>
      <password>testpw</password>
    </server>

  </servers>
 
</settings>

 

- 등록 후 target/generated-soruces/archetype 경로에서 mvn deploy를 하면 사설저장소에 저장된다.

- 사설 저장소에 있는 것을 사용 및 확인하고 싶다면 setting.xml에 mirror를 작성해줘야 한다.

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
                      http://maven.apache.org/xsd/settings-1.0.0.xsd">


  <localRepository>${user.home}/.m2/repository</localRepository>

 <mirrors>
	<mirror>
	<id>test-snapshots-repo</id>
	<mirrorOf>*</mirrorOf>
	<name>Mirror.</name>
	<url>http://127.0.0.1/repository/maven-snapshots/</url>
	</mirror>
</mirrors>

  <servers>
    <server>
      <id>test-releases</id>
      <username>admin</username>
      <password>testpw</password>
    </server>
     <server>
      <id>test-snapshots</id>
      <username>admin</username>
      <password>testpw</password>
    </server>

  </servers>
 
</settings>

 

- 등록 후 mvn archetype:generate 하면 등록한 archetype 항목이 나올것이다.

 

4. 프로젝트 생성

- mvn archetype:generate 명령어 후 플젝 선택시 입력하는 것들은

target/generated-soruces/archetype 폴더의 pom.xml에 있는

- groupid

- artifactId

- version

이며, 똑같이 입력해주면 프로젝트가 생성된다.

 

이클립스에서 Kotlin + Spring Boot 2.0 + Webflux 사용방법이다.

DB연동은 다음에 하며 간단한 프로젝트 생성 후 Get, Post, Put, Delete 부터시작한다.

 

R2DBC MySQL 연결 -

2020/06/04 - [Develop/Spring Boot] - Kotiln Spring Boot Webflux r2dbc(MySQL)

 

Kotiln Spring Boot Webflux r2dbc(MySQL)

이전 글에 이어서 Kotlin + Spring Boot 2.0 + Webflux + R2DBC(MySQL)사용방법이다. 이전글 - Kotlin Spring Boot Webflux (Eclipse) 2020/04/26 - [Develop/Spring Boot] - Kotlin Spring Boot Webflux (Eclipse..

wky.kr

 

1.

https://start.spring.io/

접속하여 간단히 프로젝트 부터 생성한다. Dependencies는 Webflux를 위해 Reactive Web만 추가한다.

 

GENERATE를 눌러 프로젝트를 다운받은 후 이클립스에 추가한다.

 

2. 실행하기 전에 Intellij 에서는 실행을 안해봐서 잘 모르겠지만, 이클립스에서 실행할 경우 final 에러가 뜨는데 그 경우

Spring Boot를 실행하는 메인 클래스에 open이라는 키워드를 붙여준다.

 

3.다음 간단히 프로젝트 구조를 잡아준다.

Model에는 Spring이나 Webflux가 아닌 SpringBoot 에서의 VO를 작성해준다.

 

4. Handler를 작성해준다. Handler는 Spring이나 Webflux가 아닌 SpringBoot 에서의 Controller라고 생각하면된다.

 

5. router를 작성해준다. router에서 URL을 설정해 Handler로 연결 시켜준다.

Configuration Annotation을 사용하는 경우 class 앞에 꼭 open 을 붙여주어야 하며,

open이 붙은 클래스들의 함수들 또한 앞에 open을 꼭 붙여주어야 한다.

 

 

6. Postman으로 간단히 실행해본다.

GET : Handler에서 작성한대로 query parameter를 넣어 그 결과를 반환한다.

 

POST : Handler에서 작성한대로 id, name을 전송하게 되면 id, name을 출력한다.

 

PUT : Handler에서 작성한대로 id, name을 전송하게 되면 id, name을 출력한다.

 

DELETE : Handler에서 작성한대로 id, name을 전송하게 되면 id, name을 출력한다.

 

다음에 시간나면 데이터베이스 연결을 위해 ReactiveCrudRepository사용법과 Join하는 방법을 올려야겠다.

 

Kotlin Webflux R2DBC MySQL - 

2020/06/04 - [Develop/Spring Boot] - Kotiln Spring Boot Webflux r2dbc(MySQL)

 

Kotiln Spring Boot Webflux r2dbc(MySQL)

이전 글에 이어서 Kotlin + Spring Boot 2.0 + Webflux + R2DBC(MySQL)사용방법이다. 이전글 - Kotlin Spring Boot Webflux (Eclipse) 2020/04/26 - [Develop/Spring Boot] - Kotlin Spring Boot Webflux (Eclipse..

wky.kr

 

Spring boot에서 AWS Elastic Beanstalk 사용시 .ebextensions 사용하는 방법이다.

Elastic Beanstalk 사용시 배포방법이 war파일 배포방법과 jar파일 배포방법이 있는데

이 두개는 .ebextensions 사용시 기본적인 사용법은 같으나 배포시 차이점이 있다.



1. 2018/10/26 - [Develop/Spring Boot] - Spring Boot AWS Elastic BeanStalk을 이용한 배포

2. 2019/02/12 - [Develop/Spring Boot] - Spring boot ElasticBeanstalk 환경변수설정시 주의사항



1.  기본 사용방법으로는 .ebextensions 란 폴더를 프로젝트 최상위에 만든다. 




2. 만들었으면 이제 서버에 적용하고 싶은 값들을 .config 확장자로 만들어 .ebextensions의 폴더에 넣으면 된다.

기본적으로 적용하고싶은 파일들의 순서는 알파벳순서라서, 보통 00-이름.config, 01-이름2.config 이렇게 이름을 짓는다.


[Directory구조 예시]

Project

--.ebextensions 

-- 00-이름.config

-- 01-이름2.config


3. 이제 nginx설정하는 방법이다. (이것이 필요 없다면 바로 4번의 배포 방법으로 가시면 됩니다.)

아래의 사진처럼 폴더를 생성한다.


3-1 . 먼저 nginx 설정을 변경하고싶다면 nginx의 전체 속성을 일단 복사해서 가져온 뒤

nginx.conf라는 파일을 생성 후 복사한 전체 내용을 ngnix.conf에 붙여넣고 변경하고 싶은 내용을 추가하면 된다. 

위 사진처럼 nginx 폴더(conf.d폴더가아님) 바로 아래에 nginx.conf 파일을 넣는다. 이렇게 되면 nginx 의 전체설정을 서버에 완전히 쓰게 된다.


3-2. nginx 전체 설정 가져오기가 번거롭다면 위 사진처럼 conf.d 폴더와 그리고 아래에 elasticbeanstalk이라는 폴더를 생성한다.

conf.d폴더아래에 만약 .conf 파일을 생성한다면 이 내용은 nginx.conf 전체 속성의 http 단에 들어가고

elasticbeanstalk 폴더 아래에 만약 .conf 파일을 생성한다면 이 내용은 nginx.conf 전체 속성의 http 단의 sever 단에 들어간다


이게 무슨말이냐면 아래의 사진처럼 conf.d 폴더에 생성한 .conf 파일의 내용은 검정색 선 안에 들어가고

elasticbeanstalk 폴더 아래에 생성한 .conf 파일의 내용은 빨간색 선 안에 들어간다.



이런식으로 nginx 설정을 변경한다.


4. 배포는 war파일 배포와 jar파일 배포시 차이가 있다. 일단 공통적으로 zip 을 해주어야 한다.



4-1. war 파일은 .ebextensions 폴더와 함께 zip 파일로 만든후 이를 Elastic Beanstalk에 올리면 ebextensions 에서 설정한 값이 적용된다.



[.zip 파일 directory 예시]

.zip

-- .ebextensions 

-- .war


4-2. jar파일은 zip파일로 만들고 배포하면 설정이 먹히지 않는다. 그렇기 때문에 Procfile 이라는 것을 사용해야 한다.

Procfile을 말 그대로 Procfile이라고 프로젝트 최상위 경로에 생성한다 


[Directory구조 예시]

Project

--.ebextensions 

-- 00-이름.config

-- 01-이름2.config

--Procfile


생성 후 Procfile 내용안에 아래와 같이 작성한다.


web: java -jar jar파일경로/파일이름.jar


작성하였으면 이제 .ebextensions 폴더와 .jar파일 Procfile을 zip 파일로 압축한다.


[.zip 파일 directory 예시]

.zip

-- .ebextensions 

-- .jar

-- Procfile


그리고 이를 Elastic Beanstalk에 올리면 ebextensions 에서 설정한 값이 적용된다.



(어차피 zip파일로 압축할거면 굳이 root 폴더에 생성해야하는 이유가 무엇인지 궁금하다.

build 시 자동으로 zip 파일로 압축하는 방법이 있다고 하던데 알아봐야겠다.)



참고 :  https://docs.aws.amazon.com/ko_kr/elasticbeanstalk/latest/dg/create_deploy_Java.html

  1. YBHwang 2019.11.13 03:21 신고

    jar로 배포하려는 1인입니다만, 저렇게하면 잘 배포되시나요??
    기본적으로 build/libs에 .jar파일이 추출되어서 /procfile와 /.ebextentions로 구성한 프로젝트를 배포해보면 항상 에러뜨더라구요ㅜ
    추가로 eb deploy로는 아예 배포가 안되구요... 너무 난감합니다ㅠㅠ

    • 팡연 2019.11.13 18:54 신고

      흠.. 네 저같은 경우는 이 글과 같이
      .zip 파일안에 Procfile파일과 .ebextensions폴더 000.jar 파일 이 3개 파일만 넣은채로 elastic beanstalk에 올렸을 경우 배포가 되었습니다.
      eb deploy가 되지 않는다면 aws elastic beanstalk 사이트에서 직접 한번 구성해서 배포를 한번 테스트 해보세요.
      도움이 된 것같진 않아 죄송합니다.


Spring boot ElasticBeanstalk 환경변수설정시 주의사항이다.

이것 때매 조금 고생을 한 적이 있다.


Spring boot에서 application.properties 작성시 대표적으로 server.port 등 이렇게 . (마침표)을 써서 사용한다

하지만 ElasticBeanstalk 에서는 아래의 사진처럼 이를 _ (언더스코어)로 구분하는데

Spring boot의  application.properties에는 - (하이픈)이 사용되는 것이 있다. 대충 이런것들 spring.aop.proxy-target-class이 있다.

이럴 경우 - (하이픈)은 . (마침표)가 아니라 그냥 두는 경향이 있는데 

이 또한 무조건 - (하이픈)을 _ (언더바)로 바꾼뒤 ElasticBeanstalk 의 환경변수설정에 입력해야 한다.

ex) SPRING_AOP_PROXY_TARGET_CLASS







LocalDateTime 의 형식 때문에 Gson으로 JSON으로 변경시 아래의 사진 처럼 Data가 Deserialize 하게 나온다.



처음엔 Gson 때문인지 몰랐고 jackson-datatype-jsr310 이거 때문인 줄 알아, 정말 쌩고생을 했음에도 불구하고 안되다가

혹시나 해서 LocalDateTime 형식 때문에 Json으로 변경시 이상하게 되는가 싶어 확인했다가 정말로 Gson때문인 걸 알게되었다.

이유는 아마도 LocalDateTime 형식에서 2019-01-06T00:24:38 가운데 T 때문에 Gson에서 자동으로 위의 사진처럼 date와 time으로

나누어 주는 것 같다.



1. JPA를 사용할 것이고, DB의 날짜 형식은 TimeStamp를 사용한다. 일단 Converter를 만들어야 한다. Spring boot에서의 JPA는 2.1을 사용하기 때문에 JPA 2.1은 Java8에서의 LocalDateTime을 지원하지 않기 때문에 Converter를 만든다. 


Converter를 만드는 법은 아래와 같이 어노테이션을 추가하거나


@SpringBootApplication
@EntityScan(basePackageClasses = {Application.class, Jsr310JpaConverters.class} )
public class Application {

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


아래와 같이 직접 만든다.


import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;

@Converter(autoApply = true)
public class DateConverter implements AttributeConverter<LocalDateTime, Timestamp> {
	@Override
	public Timestamp convertToDatabaseColumn(LocalDateTime locDateTime) {
		return (locDateTime == null ? null : Timestamp.valueOf(locDateTime));
	}

	@Override
	public LocalDateTime convertToEntityAttribute(Timestamp sqlTimestamp) {
		
		return (sqlTimestamp == null ? null : sqlTimestamp.toLocalDateTime());
	}
}


2. 만들었으면 데이터베이스를 통해 Date를 가져오는 것은 문제 없이 작동된다.

하지만 값을 찍어보면 Deserialize 하게 나온다. Gson을 사용하지 않고 순수하게 값만 가져올 경우는 이제

pom.xml에 아래의 maven을 추가한 뒤


       

			com.fasterxml.jackson.datatype
			jackson-datatype-jsr310
 

application.properties에 아래처럼 추가하면 Deserialize 하게 나오던 값이 serialize 나온다.

spring.jackson.serialization.write-dates-as-timestamps=false

하지만 우리가 할 것은 Gson을 통해 변경하는 것이기 때문에 위와 같이 하여도 아무런 변화를 느끼지 못할 것이다.


3. 이제 Gson을 통해 변경하기 위해 클래스를 생성한다.


import java.lang.reflect.Type;
import java.time.LocalDateTime;

import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;

public class GsonConfig implements JsonSerializer<localdatetime>{

	@Override
	public JsonElement serialize(LocalDateTime date, Type type, JsonSerializationContext json) {
		// TODO Auto-generated method stub
		return new JsonPrimitive(date.toString());
	}

}


그리고 이제 Gson을 사용시 아래처럼 사용한다.


Gson gson = new GsonBuilder().registerTypeAdapter(LocalDateTime.class, new GsonConfig()).create();
String jsonString = gson.toJson("변경할 값");

4. 이렇게 하면 2019-01-06T00:24:38 가운데 T 때문에 맨위의 사진처럼  Deserialize 나온 값이

 아래의 사진처럼  Serialize하게 나온다.



Spring boot 에서  Invisible reCAPTCHA 설정하는 방법이다.



reCAPTHA 는 쉽게 말해 봇이 접근을 시도하는 것으로부터 보호하여 스팸글 등록 및 자동 글 작성 자동 회원가입을 막는 것이다.


현재는 v3까지 나왔으며 여러버전이 있는 데, Invisible reCAPTCHA는 말그대로 보이지 않는다. 버튼 체크나, 그림 맞추기 등을 하지 않아도 됨.

원리는 구글측에서 공개하지는 않았지만 구글측에서 사용자의 패턴을 분석해서 방지한다는 정도로 알고있다.



1.  https://www.google.com/recaptcha/intro/v3.html 사이트에 접속한다.


우측 상단의 My reCAPTCHA에 접속한다.



2.  접속한 뒤 새로 만든다.


Label 에는 제목을 입력한다. 이 제목은 말그대로 그냥 제목이다 파일 이름 짓듯이, 프로젝트 생성하듯이 지으면 된다.

그리고 아래와 같이 기입한 후 생성한다. Send alerts to owner는 체크해도 되고 안해도된다. 그리고 만들고나서도 수정 가능하니 신경을 크게 안써도 된다.



3.  생성된 것을 확인

아래와 같이 Site key와 Secret key를 받게 된다.

Step1에는 사용방법이 간단하게 나와 있으며 

Step2에는 Invisible reCAPTCHA API에 사용에 필요한 요청과 응답이 나와 있다.


4.   https://developers.google.com/recaptcha/docs/invisible 옆의 주소에 들어간다.

구글에서 아래와 같이 기본적인 예제를 올려 놓았다. 

이것을 복사를 한다.



5. 복사를 했으면 Spring boot의 static 폴더 아래에 아래와 같이 만든다

굳이 여기가 아니더라도 프론트엔드를 따로 사용하시는 분들이라면 그 쪽에 만들어서 사용하면 된다.


data-siteKey는 3번에서 받은 Site-key를 입력한다. 그리고 action에는 reCAPTCHA에 사용할 API주소를 입력한다. 


6. action에 사용할  reCAPTCHA를 사용할 Controller를 만든다.

하지만 그 전에 reCAPTCHAP에 응답받을 DTO를 생성한다. 이 작업은 굳이 안해도 되고 String으로 바로 받아도 되지만

깔끔함을 위해 작성했다.

구글 API에 아래와 같이 나와있다. 

위를 바탕으로 DTO를 작성한다.

public class reCAPTCHA_DTO {
	private boolean success;
	private Integer number;
	private String action;
	private String challenge_ts;
	private String hostname;
	private String error_codes;
	
	
	public reCAPTCHA_DTO() {
		super();
	}


	public reCAPTCHA_DTO(boolean success, Integer number, String action, String challenge_ts, String hostname,
			String error_codes) {
		super();
		this.success = success;
		this.number = number;
		this.action = action;
		this.challenge_ts = challenge_ts;
		this.hostname = hostname;
		this.error_codes = error_codes;
	}


	public boolean isSuccess() {
		return success;
	}


	public void setSuccess(boolean success) {
		this.success = success;
	}


	public Integer getNumber() {
		return number;
	}


	public void setNumber(Integer number) {
		this.number = number;
	}


	public String getAction() {
		return action;
	}


	public void setAction(String action) {
		this.action = action;
	}


	public String getChallenge_ts() {
		return challenge_ts;
	}


	public void setChallenge_ts(String challenge_ts) {
		this.challenge_ts = challenge_ts;
	}


	public String getHostname() {
		return hostname;
	}


	public void setHostname(String hostname) {
		this.hostname = hostname;
	}


	public String getError_codes() {
		return error_codes;
	}


	public void setError_codes(String error_codes) {
		this.error_codes = error_codes;
	}
}

7. 이제 controller를 작성한다.


params 의 seceret에서는 3번의 seceret_key를 적는다.

3번에의 요청에서 봤듯이 ip가 필요하기 때문에 ip를 받아온다. (ip받아오는 방법은 서버마다 다르기 때문에 굳이 이렇게 안해도 됨.)

g-recaptha-response역시 3번에 나와 있듯이 구글측에서 주는 것이기 때문에 RequsetParam으로 받아 그대로 사용하면 된다.

그리고 아래와 같이 작성해서 post를 날리면 된다.


@PostMapping() public reCAPTCHA_DTO reCAPTCHA_TEST(@RequestParam(name="g-recaptcha-response") String recaptchaResponse , HttpServletRequest request) { String ip = request.getRemoteAddr(); String url = "https://www.google.com/recaptcha/api/siteverify"; String params="?secret=6LcwQYIUAAA******************&response="+recaptchaResponse; RestTemplate restTemplate = new RestTemplate(); reCAPTCHA_DTO re = restTemplate.exchange(url+params, HttpMethod.POST, null, reCAPTCHA_DTO.class).getBody(); if(re.isSuccess()) { System.out.println("성공"); } else { System.out.println("실패"); } return re; }

8. 결과


아래 사진과 같이 맨 오른쪽 하단에 reCAPTCHA 표시가 나타나며 submit을 누르면 결과가나온다.

결과



Invisible reCAPTCHA말고도 다른 reCAPTCHA사용하는 방법 역시 위와 크게 다르지 않기 때문에 아래의 레퍼런스에서 확인하여 때에 맞게 사용하면 된다.



참고 -  https://developers.google.com/recaptcha/intro


먼저 이 글을 보기 전에 알고 있으면 더 좋은 글입니다.


1번. MySQL JSON DATA TYPE

2018/11/30 - [Develop/MySQL] - [MySQL] MySQL JSON - MySQL 5.7 JSON Functions



2번. MySQL에서의 '일반' 값을 JSON형식으로 가져오기

2018/10/28 - [Develop/Spring Boot] - Spring boot JPA EntityManager를 이용한 Map형식으로 mapping하기

2018/10/24 - [Develop/Spring Boot] - Spring boot jpa map, hashmap, JSON형식


3번. JSON PARSING

2018/11/13 - [Develop/Spring Boot] - Spring Boot Json, hashmap to json , JsonObject 만들기- JSON 마지막

2018/11/12 - [Develop/Spring Boot] - Spring Boot Deserialization Json, Deserialization JsonString to JsonObject - Json 4편

2018/11/09 - [Develop/Spring Boot] - Spring Boot Json, Jackson을 이용한 JsonParsing - Json 3편

2018/11/07 - [Develop/Spring Boot] - Spring Boot Json, Gson을 이용한 JsonObject to String, String to JsonObject- JSON 2편

2018/11/06 - [Develop/Spring Boot] - Spring Boot Json, JsonObject로 만들기 - JSON 1편



나도 SPRING BOOT 에서 어떻게 가져오는지 많이 검색해봤으나. 자료가 거의 없어서 열심히 삽질했던 것 같다.

수 많은 삽질 끝에 알아내서 필요한 곳에 사용중이다.


이번 글은 MySQL에서 SELECT시 일반 DATA 형식이 아닌 JSON DATA TYPE을 SPRING BOOT 에 SELECT하는 내용이다.

우선 일반 DATA형식은 그냥 말그대로 SELECT id, name FROM test 해서 가져오는 내용들이고

JSON DATA TYPE 형식은 위의 1번 링크에서 확인할 수 있다.



1. 우선 이번에 사용할 예제 테이블 및 데이터이다. 1번링크에서 확인 가능하다.



2. 먼저 JSON DATA TYPE을 불러올 코드를 작성한다.


아래의 코드를 보면 CONCAT_WS('\\\\',  부분이 있다.


사실 JSON DATA TYPE을 가져올 때 JSON_OBJECT를 사용하면 가져올 수 있으나. 사용하는 이유는


SPRING BOOT에서 CONCAT_WS('\\\\',  를 사용하지 않으면 MySQL에서 값을 불러올때 쌍따옴표를 지워버린다. 


그래서 만약 CONCAT_WS를 사용하지않고 JSON_OBJECT만을 이용하여 SELECT하면


ex) {"ID": 1, "NAME": "TEST", "PASSWORD": "1234", "PHONE_NUM": "010-1111-1111"} 이렇게 불러와야할 값이

     {"    이렇게 나오고 뒤의 내용은 다 짤려버린다.


그래서 CONCAT_WS 함수를 이용하여 쌍따옴표가 안짤리고 SELECT하기 위해 앞에 역슬래쉬를 붙인다. 


4개 붙이는 이유는 두개 붙이니까 SPRING BOOT단에서 짤려버리더라...... 그래서 4개 붙인다.


그리고 CONCAT_WS를 사용하면 값을 Byte로 불러온다.  그래서 String 형식이나 기타 외의 데이터로는 불러오지 못하므로


항상 Object 데이터 형식을 사용해서 불러와야 한다. 그 후 Object를 Byte로 변환 그리고 Byte를  String으로 변환하는 방법 등을 사용 해야 한다.


아래의 코드에서도 우선 List로 결과를 불러오고 Object를 통해 Byte로 변환 그리고 Byte를 다시 String으로 변환한다.


@GetMapping("/test")
	public void test() {
		Query query = entityManager.createNativeQuery("SELECT " + "concat_ws('\\\\', "
				+ "json_object('id', t.id, 'password', t.password, 'name', t.name, 'phone_num', t.phone_num)) "
				+ "as TEST " + "FROM " + "test t");

		List r = query.getResultList();
		for (Object obj : r) {
			byte[] b = (byte[]) obj;
			String value = new String(b, StandardCharsets.UTF_8);
			System.out.println(value);
		}
}


위 코드를 실행하면 아래와 같은 JSON 타입의 내용을 Spring Boot에서 불러올 수 있다.




3.  이 Json String을 JsonObject로 만들어보자 


3번 링크의 Gson이나 Jackson, JsonObject 등등 을 이용하여 만들 수 있다.


Jackson을 이용하여 한번 만들어 보겠다.


먼저 TestDTO 클래스를 하나 만든다 

3번링크의 Json 1편의 DTO에서 password와 phone_num 변수를 추가한다.


만든 후 현재 데이터를 2개가 불러오기 때문에 객체 배열로 생성하였다.

그 후 각 클래스 배열마다 Jackson의 objectmapper를 사용하여 Json String을 Json Object로 변환시켜주었다.


Query query = entityManager.createNativeQuery("SELECT " + "concat_ws('\\\\', "
				+ "json_object('id', t.id, 'password', t.password, 'name', t.name, 'phone_num', t.phone_num)) "
				+ "as TEST " + "FROM " + "test t");


		List r = query.getResultList();
		
		ObjectMapper objectMapper = new ObjectMapper();
		TestDTO t[] = new TestDTO[r.size()];
		int i=0;
		
		for (Object obj : r) {
			byte[] b = (byte[]) obj;
			String value = new String(b, StandardCharsets.UTF_8);
			try {
				
				t[i]= objectMapper.readValue(value, TestDTO.class);
				i++;
			}
			catch (IOException e) {
				e.printStackTrace();
			}
		}
		for(TestDTO test : t) {
			System.out.println(test.getId());
			System.out.println(test.getPassword());
			System.out.println(test.getName());
			System.out.println(test.getPhone_num());
			System.out.println();
		}

아래와 같이 결과가 나온다


이것을 많은 형식을 이용하여 불러 올 수있다 2번링크를 사용하여 애초에  Map으로 불러와서 mapping시키거나 할 수 있다.


Map으로 불러와서 mapping할 경우 문제점은 정규식을 통해 파싱해줘야 하는 단점이있다.


왜냐하면 Json값이 { "id" : 1 } 이렇다면 Map으로 불러올경우 "{ "id" : 1 }" 이렇게 중괄호에 쌍따옴표가 붙어버린다.


그래서 정규식을 통해 중괄호의 쌍따옴표만 지워야 하는 단점이 있다. 하지만 나는 이러한 단점에도 불구하고도 


JSON_TYPE과 함께 일반 데이터까지 가지의 값을 가져 와야하기 때문에 자주 사용한다.

+ Recent posts