정말 오랜만의 포스팅입니다.


사실 너무 바빴습니다..


현재 회사 서비스의 Backend, Infra(AWS), Frontend 모두 담당하고 있고..


거기다가 친구와 같이하는  서비스의 Backend 작업하고 있다보니 굉장히 바쁩니다...ㅠㅠ


그래서 앞으로 언제 또 포스팅을 쓸지 기약이 없습니다...




사설은 그만하고 오늘 쓸 포스팅은 스프링 시큐리티를 사용하여 remember-me 기능을 구현하고


추가로, was 를 재기동(재시작) 하더라도 자동 로그인을 가능하게 하는 샘플 프로젝트입니다.



다음과 같은 샘플코드를 작성하게 된 경위는


보통 인프라를 구성할때 AWS 를 사용하든 안하든 결국 L4 또는 L7과 같은 스위치 및 그와 비슷한 기능을 하는 것들을 사용할 수 밖에 없습니다.


저는 AWS 의 ELB를 사용하기도 하고 nginx 의 upstream 기능을 사용하기도 합니다.


서버 이중화는 선택이 아닌 필수니까요.


보통 Backend의 restful 서비스의 경우는 stateless(session-less) 이기 때문에 별로 상관이 없습니다.


무중단 배포가 생각보다 쉽게 됩니다. 


그러나 웹이라면 좀 달라집니다. 생각외로 많은 웹 서비스들은 session 에 많이 의존하고있고, 


was 가 재기동을 하게되면 session 정보를 잊어버리면서 다시 로그인해야 하는 상황이 벌어집니다. 



물론 S/W 방식으로 구현하는 방식이 있고, H/W 방식으로 구현하는것도 있죠 (예를들면 Session 을 외부 Storage 에 저장한다던가? redis ??)


한가지 확실한것은 둘다 까다롭기는 마찬가지입니다. 



본론으로 들어가면, 직접 구현하는것은 비추합니다. Spring Security 같은 굉장한 솔루션을 두고도 직접 구현하는건... (바퀴를 다시 만들지 마세요)



핵심만 바로 들어갑니다.



@Service
public class PersistTokenRepository implements PersistentTokenRepository {

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

	@Autowired
	private StringRedisTemplate stringRedisTemplate;

	@Override
	public void createNewToken(PersistentRememberMeToken token) {
		RememberToken rememberToken =
				new RememberToken(token.getUsername(), token.getSeries(), token.getTokenValue(), token.getDate());
		stringRedisTemplate.opsForValue().set(token.getSeries(), JsonUtils.toJson(rememberToken), 30, TimeUnit.DAYS);
	}

	@Override
	public void updateToken(String series, String tokenValue, Date lastUsed) {
		String payload = stringRedisTemplate.opsForValue().get(series);
		try {
			RememberToken rememberToken = JsonUtils.fromJson(payload, RememberToken.class);
			rememberToken.setTokenValue(tokenValue);
			rememberToken.setDate(lastUsed);
			stringRedisTemplate.opsForValue().set(series, JsonUtils.toJson(rememberToken), 30, TimeUnit.DAYS);
			logger.debug("Remember me token is updated. seried={}", series);
		} catch (JSONException e) {
			logger.error("Persistent token is not valid. payload={}, error={}", payload, e);
		}
	}

	@Override
	public PersistentRememberMeToken getTokenForSeries(String seriesId) {
		String payload = stringRedisTemplate.opsForValue().get(seriesId);
		if(payload == null) return null;
		try {
			RememberToken rememberToken = JsonUtils.fromJson(payload, RememberToken.class);
			PersistentRememberMeToken token = new PersistentRememberMeToken(
					rememberToken.getUsername()
					,seriesId
					,rememberToken.getTokenValue()
					,rememberToken.getDate());
			return token;
		} catch (JSONException e) {
			logger.error("Persistent token is not valid. payload={}, error={}", payload, e);
			return null;
		}
	}

	@Override
	public void removeUserTokens(String username) {
		// Skip this scenario, because redis set only unique key.
	}

}



4가지만 기억하면 됩니다.


토큰을 발급하고 > 발급한 토큰을 조회하고 > 사용한 토큰을 다시 사용할 수 없게 수정하고 > 혹시나 탈취되거나 만료된 경우는 삭제한다.


PersistentTokenRepository 을 사용하기 위해서는 어쩔수없이 외부의 저장소가 있어야 합니다. 

(스프링의 경우 In-memory 또는  JDBC 방식을 제안합니다.)


하지만 JDBC로 하게되면 Entity(Table) 도 만들어야 하고.. 이래저래 좀 그렇죠 사전작업도 해야되니까요


요새는 워낙에 캐싱을 많이 쓰니 memcached 또는 redis 는 한번쯤 접해보셨을 거라고 생각합니다.


그래서 redis 를 사용해서 구현하였고요, 생각보다 쉬우니 


자동로그인을 구현해야 하는데 서버는 이중화 되어있고, 


무중단 배포를 하고싶은데 사용자들이 로그아웃 되게 하고 싶지 않은경우에는 한번 살펴보세요~


생각보다 쉽게 해결될 수도 있으니까요



https://github.com/okihouse/spring-boot-security-with-redis

저작자 표시
신고

오랜만의 포스팅입니다~ 


오늘은 querydsl 에 대한 내용을 적어보려고 합니다~


저의 포스팅은 거의 그렇듯 아래와 같이 이루어집니다.




기존코드     >     문제발견     >     개선코드

 



짧게 설명을 하자면...


개선된 코드만 보는 습관을 들이게 되면 과거를 놓치게 되고 똑같은 실수를 저지르게 됩니다.


사실 우리나라의 거의 모든 스프링 프레임워크를 쓰는 개발자들이 스프링의 위대함을 얼마나 느낄지 모르겠으나...


스프링이 없던 시절을 생각해 보면.... 정말 암울합니다...


현재는 서비스 코드만 집중하는 시대에 살고 있으니까요... 스프링이 나머지를 다 해주니.. (비단 자바진영뿐만 아니고 나머지도 마찬가지죠)




여하튼~ 


JPA 를 쓰다보면 누구나 하나둘 문제에 부딪히게 됩니다... 한번도 문제를 만난적이 없으시다면 당신은 초고수 


오늘 포스팅은 조인에 대한 이야기 입니다. 거창하지 않고 간단하게 할겁니다.


먼저 엔티티부터 볼까요??



@Entity
@Table(name = "user")
@Data
public class User {

	@Id
	@GeneratedValue
	@Column(name = "uno")
	private Long userNo;

	@Column(name = "user_type")
	@Enumerated(EnumType.STRING)
	private USER_TYPE type;
	
	@OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
	private UserInfo userInfo;
	
	public enum USER_TYPE {USER, ADMIN}
	
}

@Entity
@Table(name = "userinfo")
@Data
public class UserInfo {

	@Id
	private Long userNo;

	@Column(name = "EMAIL")
	private String email;

	@MapsId
	@OneToOne(cascade = CascadeType.ALL)
	@JoinColumn(name = "UNO")
	private User user;
	
}




굉장히 심플한 2개의 엔티티 입니다~ 


1:1 관계를 갖고있으며, 컬럼도 많이 없습니다. 


저 두개를 한번 조인해 볼까요??



@Query(value = "select new com.boot.jpa.vo.ResultVO(" + "u.userNo," + "u.type," + "ui.email) " + "from User u inner join u.userInfo ui " + "where u.type = :type" ) List<ResultVO> findByType(@Param("type") USER_TYPE user);



보시다 시피 좀 지저분하죠...?? 하지만 누군가에게는 굉장히 익숙한 방법입니다~


직접 쿼리를 쓰는(사용하는) 분들에게는 모두 익숙하고 가독성도 나쁘지 않을겁니다.


그러니까 전혀 문제가 없다고 생각 할 수도 있습니다.. 



그러나 위 코드는 몇몇개의 문제가 존재합니다. 바로 Dynamic Query 를 작성해야 할때가 그 중 하나입니다.


만약에 type 값이 있을때만 where 구문을 하고 싶다면?? 


select 쪽에 if 또는 when 을 넣고싶다면??? 



그렇기에 저 코드는 조금 문제가 있습니다. 물론 아예 방법이 없는 것은 아니지만... 그리 좋은방법도 아니기에~


또한 JPA 의 철학?? 에 빗대어 보자면 우리는 객체지향언어 개발자들이니... 쿼리도 객체지향적으로 다뤄야 겠지요~ (직접 쿼리를 쓰는건 넘나 안되는것)




이미 많은 개발자분들 및 선배님들이 이 문제를 알고 대응을 하셨습니다. 


JPA책을 보신 분들이라면 아시겠지만 QueryDSL 말고도 여러 방법이 있습니다. 


하지만 오늘은 간단하게 QueryDSL 만 하려고요~ 이게 제일 좋아보여서요 ㅋㅋ



각설하고 코드부터 볼까요??


@PersistenceContext private EntityManager entityManager; private final QUser qUser = QUser.user; private final QUserInfo qUserInfo = QUserInfo.userInfo; @Override public List<ResultVO> findByType(USER_TYPE user) { JPAQuery query = new JPAQuery(entityManager); return query.from(qUser) .innerJoin(qUser.userInfo, qUserInfo) .where(qUser.type.eq(User.USER_TYPE.USER)) .list(Projections.bean(ResultVO.class, qUser.userNo, qUser.type, qUserInfo.email)); }



선언부를 다 제외하고 return 부분만 보시면 됩니다. 


위에서 보여드린 쿼리랑 동일한 동작을 하는 코드입니다. 


객체지향적으로 작성되었지만 보시는대로 쿼리랑 다를바가 없습니다. (굳이 설명하지 않아도 바로 이해되는코드... 정말 대단하지 않나요???)



기존코드의 문제점은 이제 모두 해결됩니다. 예를들어 Dynamic Query?? 문제가 되지 않습니다. 평소 우리가 하던대로 if 넣으면 됩니다. 



QueryDSL을 안쓰면(직접 쿼리를 쓰면) 나쁜코드를 작성하는 사람이냐?? 제 관점에서는 아닙니다.


하지만 JPA 관점에서는 그렇게 보일 수도 있습니다. 스프링의 관점에서도 마찬가지 일 것 이라고 추측합니다. 


스프링이 XML 과 작별을 한 것 처럼 JPA는 쿼리와(DB Schema) 작별하려고 합니다. 종속되지 않으려고 하는거죠


기본전제는 단순합니다. 너는 객체지향 개발자이다. 객체지향적으로 코드를 작성하라!! 




샘플코드를 github 에 올려놓았습니다. 


직접 코드를 실행시켜 보시고 jpa 와 한발 더 친해져 보세요~ (샘플코드에는 pageable 에 대한 내용도 나와있습니다~)


https://github.com/okihouse/spring-jpa-querydsl-sample





저작자 표시
신고

늦었지만~ 새해 복 많이 받으세요 ^^


오늘 포스팅은 JPA 와 Testing 에 관한 것 입니다.



최근들어 JPA가 많이 사용되는 것 같습니다~ 제가 속한곳의 프로젝트도 JPA로 작성하고 있구요~


그래서~ 오늘은 JPA 로 구현된 코드를 어떻게 테스트 할 것인지에 대한 내용을 포스팅 하려고 합니다.


이전 포스팅에서도 예고했듯이 오늘의 핵심은 바로 MockMvc & Transaction Testing 입니다.


먼저 일반적인 시나리오를 하나 볼까요?? (사용자 로그인 시나리오)

1. 사용자가 이메일 + 비밀번호를 입력하여 로그인 합니다.

2. 사용자의 정보가 DB에 존재한다면 정상 응답을 받을것이고 (2xx ok)

3. 사용자의 정보가 DB에 존재하지 않는다면 비정상 응답을 받을 것 입니다.(4xx client error) - e.g 가입되지 않은 사용자입니다 등등.. 


굉장히 심플하고 실제로 운영코드에도 사용되는 지극히 정상적인 시나리오 입니다 ㅎㅎ



 경우의 수 에 대한 생각을 한번 해볼게요~

1. 요청 정보가 제대로 되지 않은경우(일부 파라미터가 누락되었다거나... 요청방식이 잘못되었다거나....)

2. 사용자의 정보가 DB에 존재 및 존재하지 않는경우.

3. 기타 등등 


뭐 딱히 복잡한게 없습니다... 에러처리할 것도 별로 없고요~ 다음으로 넘어가볼까요?



기존 테스트 방식은 어떨까요?? 

글세요 이것은 사람마다 너무 달라서.... 제 방식으로만 설명드리자면... 크게 2가지 일텐데


- 로직검증 > 모듈검증 > 실제 환경을 수행하는 메소드나 클래스 단위로 검증

- 요청검증 > 실제로 요청 하여 DB 값 또는 반환값으로 검증


로직검증같은 경우에는 Mockito 를 많이 써서 임의의 값을 받고 넘겨주는 작업을 했으며(예를 들어서 MockHttpServletRequest, MockHttpSession 등등...)


요청검증의 경우에는 실제로 요청을 해보고 DB값을 갖고와서 Assert 에서 비교를 하거나, 반환값들을 Assert 에서 비교를 하곤 했지요~



오늘 여기서 다룰 것은~ 바로 2번째 요청검증에 대한 내용입니다.



먼저, 앞서 설명했던 사용자 로그인 시나리오를 볼까요??


일단 사용자로부터 이메일 + 비밀번호를 받아야 겠네요~


두번째로... DB에 사용자 정보가 있는지 없는지를 검증하구요!! 


마지막으로 반환값을 검증해야 합니다. 


근데!! 여기서 문제가 하나 있습니다... 로컬에서 테스트하고... 개발서버에서도 테스트하고.. 문제 없는데 만약에...


실제 운영서버에서 테스트하려면?? 

>> 임의의 사용자를 DB에 넣 그 사용자가 있는경우가 없는경우를 테스트 해야되나요?? DB에 더미값이 쌓이게 됩니다.



즉, 핵심은 이겁니다!! DB에 테스트 전 임시 사용자 값을 넣고 테스트가 종료되면 자동적으로 롤백되도록 하는거죠~



테스트 코드부터 보시죠


# Test Case 1. 회원이 존재하지 않는경우


@Test         public void test_user_not_exist() throws Exception {     // parameter     String email = "test@test.com";     // request get     mockMvc.perform( get("/user/login") .param("email", email)                 .contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) .andDo(print()) .andExpect(status().is5xxServerError()) .andExpect(jsonPath("$.message").value("user is not exist")); }


------------------------------------------------------------------------------------------------------------------------------------------------------


# Test Case 2. 회원이 존재하는경우


	@Test
	public void test_user_exist() throws Exception {
		// parameter
		String email = "user@test.com";
		
		// insert user
		User user = new User();
		user.setType(User.USER_TYPE.USER);
		
		UserInfo userInfo = new UserInfo();
		userInfo.setEmail(email);
		userInfo.setUser(user);
		
		user.setUserInfo(userInfo);
		
                // persist
		userRepository.save(user);
		
		// request get
		mockMvc.perform(
				get("/user/login")
				.param("email", email)
				.contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))
					.andDo(print())
					.andExpect(status().is2xxSuccessful())
					.andExpect(jsonPath("$.message").value("user exist"));
	}



두번째 케이스만 보시면 될 것 같습니다~


파라미터는 패스워드를 임의로 누락시킨거니 신경쓰지 마시구요 ㅎㅎ 


중요한건 Insert user 부분입니다~ 실제로 DB에 JPA 를 이용하여 사용자 정보를 입력합니다. 


그리고 나서?? Controller 를 호출하는거죠~ 


마지막으로 Assert 작업을 합니다. 2xx OK 가 되었는지~ 결과값은 정상적인지요 


테스트가 끝나면?  아래와 같이 롤백됩니다~ 걱정하지 마세요 ^^



실제 바로 테스트 할 수 있는 코드를 Github에 올려놓았습니다.


테스트 해보시고~ 실제 작업에도 연동시켜 보세요~ 깔끔한 테스트 코드가 탄생할 지도 모르자나요? ~



https://github.com/okihouse/spring-jpa-test











저작자 표시
신고

+ Recent posts

티스토리 툴바