React 는 굉장히 훌륭한 DOM 을 포함한 UI 프레임워크입니다. (A JavaScript library for building user interfaces - https://reactjs.org/


하지만 React 는 그대로 쓸수가 없습니다.. react 가 es6 기반으로 되어있기때문입니다.


물론 es5 를 쓸수도 있습니다(https://reactjs.org/docs/react-without-es6.html)


헌데 그건 문서에서도 볼 수 있듯이 굉장히 귀찮은 일이죠...



그래서 webpack 을 쓰는게 좋습니다 기존의 gulp, grant 둘다 좋지만 webpack 써본결과 훨씬좋습니다.


많은분들이 webpack 을 쓰고계시고, 저 역시 쓰고있습니다. (HMR 은 진짜 신세계입니다)





근데 이놈이 보통 어려운게 아닙니다... ㅠㅠ



특히나 Spring 을 이용하신 분들은 더 어려워 하시는 것 같더라고요 (제 기준입니다)


그래서 샘플을 하나 만들었습니다.


spring-boot + webpack + react 입니다. 부가적으로 typescript, scss 도 들어있습니다~


typescript, scss 는 정말 좋습니다 정말로요.. 안써보셨다면 꼭 써보세요~ 괜히 구글이 angular 에 typescript 를 도입한게 아니더라고요


일단 왜 webpack 이 좋은지 왜 HMR 을 써야 하는지 궁금하시다면 영상을 하나 보시죠~




https://youtu.be/hFdFXei-5m8


코드도 올려놨으니 한번 둘러보시고 적용도 해보세요~ 


https://github.com/okihouse/spring-boot-webpack-react-typescript



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


사실 너무 바빴습니다..


현재 회사 서비스의 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-jwt-for-restapi-webapp

오랜만의 포스팅입니다~ 


오늘은 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





+ Recent posts

티스토리 툴바