Skip to content
This repository has been archived by the owner on Aug 13, 2022. It is now read-only.

Commit

Permalink
Merge pull request #24 from f-lab-edu/feature/10
Browse files Browse the repository at this point in the history
#23 Feature/10
  • Loading branch information
yyy9942 authored Nov 22, 2019
2 parents 07d86e9 + 86a7154 commit dbdbe37
Show file tree
Hide file tree
Showing 40 changed files with 2,279 additions and 650 deletions.
10 changes: 8 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
Expand All @@ -66,11 +66,17 @@
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>

<dependency>
<groupId>org.codehaus.janino</groupId>
<artifactId>janino</artifactId>
</dependency>

<!-- AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/delfood/FoodDeliveryApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

@SpringBootApplication
Expand All @@ -18,6 +20,8 @@
*
* id값이 존재한다면 서버에서는 이 세션의 주인이 이 id를 가진 사용자라고 확인하게 되고 관련된 정보를 사용할 수 있도록 조회한다.
*/
@EnableAspectJAutoProxy // 최상위 클래스에 적용해야 AOP를 찾을 수 있도록 만들어준다.
@EnableCaching // Spring에서 Caching을 사용하겠다고 선언한다.
public class FoodDeliveryApplication {

public static void main(String[] args) {
Expand Down
96 changes: 96 additions & 0 deletions src/main/java/com/delfood/aop/AuthCheckAspect.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.delfood.aop;

import javax.servlet.http.HttpSession;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.delfood.service.ShopService;
import com.delfood.utils.SessionUtil;
import lombok.extern.log4j.Log4j2;

@Aspect
@Component
@Log4j2
@SuppressWarnings("unchecked")
public class AuthCheckAspect {
@Autowired
private ShopService shopService;

/**
* session에서 owner 로그인을 체크한다.
* 로그인되어있지 않을 시 해당 메서드 로직을 중지시킨 후 리턴한다.
* @OwnerLoginCheck 해당 어노테이션이 적용된 메서드를 검사한다.
* @author jun
* @param pjp
* @return 로그인시 SUCCESS, 비로그인시 NO_LOGIN
* @throws Throwable
*/
@Before("@annotation(com.delfood.aop.OwnerLoginCheck)")
public void ownerLoginCheck(JoinPoint jp) throws Throwable {
log.debug("AOP - Owner Login Check Started");

HttpSession session = ((ServletRequestAttributes)(RequestContextHolder.currentRequestAttributes())).getRequest().getSession();
String ownerId = SessionUtil.getLoginOwnerId(session);

if(ownerId == null) {
log.debug("AOP - Owner Login Check Result - NO_LOGIN");
throw new HttpStatusCodeException(HttpStatus.UNAUTHORIZED, "NO_LOGIN") {};
}
}


/**
* 세션에서 사장님 로그인을 체크 한다.
* 그 후 입력받은 파라미터 값 중 매장 id를 검색하여 해당 매장이 접속한 사장님의 것인지 검사한다.
* @author jun
* @param pjp
* @return 비로그인시 NO_LOGIN, 해당 매장의 사장이 아닐 시 UNAUTHORIZED, 권한이 있을 시 SUCCESS
* @throws Throwable
*/
@Before("@annotation(com.delfood.aop.OwnerShopCheck)")
public void ownerShopCheck(JoinPoint jp) throws Throwable {
log.debug("AOP - Owner Shop Check Started");


HttpSession session = ((ServletRequestAttributes)(RequestContextHolder.currentRequestAttributes())).getRequest().getSession();
String ownerId = SessionUtil.getLoginOwnerId(session);

if(ownerId == null) {
log.debug("AOP - Owner Shop Check Result - NO_LOGIN");
throw new HttpStatusCodeException(HttpStatus.UNAUTHORIZED, "NO_LOGIN") {};
}

Object[] args = jp.getArgs();
Long shopId = (Long) args[0];

if (!shopService.isShopOwner(shopId, ownerId)) {
log.debug("AOP - Owner Shop Check Result - UNAUTHORIZED");
throw new HttpStatusCodeException(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED") {};
}
}

/**
* 고객의 로그인을 체크한다.
* @author jun
* @param pjp
* @return
* @throws Throwable
*/
@Before("@annotation(com.delfood.aop.MemberLoginCheck)")
public void memberLoginCheck(JoinPoint jp) throws Throwable {
log.debug("AOP - Member Login Check Started");

HttpSession session = ((ServletRequestAttributes)(RequestContextHolder.currentRequestAttributes())).getRequest().getSession();
String memberId = SessionUtil.getLoginMemberId(session);

if (memberId == null) {
throw new HttpStatusCodeException(HttpStatus.UNAUTHORIZED, "NO_LOGIN") {};
}
}
}
5 changes: 5 additions & 0 deletions src/main/java/com/delfood/aop/MemberLoginCheck.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.delfood.aop;

public @interface MemberLoginCheck {

}
9 changes: 9 additions & 0 deletions src/main/java/com/delfood/aop/OwnerLoginCheck.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.delfood.aop;

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
public @interface OwnerLoginCheck {

}
15 changes: 15 additions & 0 deletions src/main/java/com/delfood/aop/OwnerShopCheck.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.delfood.aop;

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

/**
* <b>매장 id가 첫 번째 파라미터로 와야한다.</b>
* 접속한 사장님이 해당 매장의 주인인지 확인한다.
* @author yyy99
*
*/
@Target(ElementType.METHOD)
public @interface OwnerShopCheck {

}
54 changes: 49 additions & 5 deletions src/main/java/com/delfood/config/RedisConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.time.Duration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
Expand All @@ -25,6 +29,10 @@ public class RedisConfig {
@Value("${spring.redis.password}")
private String redisPwd;

@Value("${spring.redis.defaultExpireSecond}")
private long defaultExpireSecond;


/*
* Class <=> Json간 변환을 담당한다.
*
Expand All @@ -47,12 +55,16 @@ public ObjectMapper objectMapper() {
}

/*
* Redis Connection Factory library별 특징 1. Jedis - java의 표준 redis client library Connection Poll을
* 적용하여 높은 TPS를 요구하면 Redis의 CPU 점유율이 높아져 문제가 발생할 수 있다.
* Redis Connection Factory library별 특징
* 1. Jedis - 멀티쓰레드환경에서 쓰레드 안전을 보장하지 않는다.
* - Connection pool을 사용하여 성능, 안정성 개선이 가능하지만 Lettuce보다 상대적으로 하드웨어적인 자원이 많이 필요하다.
* - 비동기 기능을 제공하지 않는다.
*
* 2. Lettuce - Netty 기반 redis client library 비동기로 요청하기 때문에 Jedis에 비해 높은 성능을 가지고 있다.
* 2. Lettuce - Netty 기반 redis client library
* - 비동기로 요청하기 때문에 Jedis에 비해 높은 성능을 가지고 있다.
* - TPS, 자원사용량 모두 Jedis에 비해 우수한 성능을 보인다는 테스트 사례가 있다.
*
* Jedis와 Lettuce의 성능 비교 https://jojoldu.tistory.com/418
* Jedis와 Lettuce의 성능 비교 https://jojoldu.tistory.com/418
*/
@Bean
public RedisConnectionFactory redisConnectionFactory() {
Expand All @@ -72,7 +84,7 @@ public RedisTemplate<String, Object> redisTemplate(ObjectMapper objectMapper) {
redisTemplate.setConnectionFactory(redisConnectionFactory());
// json 형식으로 데이터를 받을 때
// 값이 깨지지 않도록 직렬화한다.
// 저장할 클래스가 여러개일 경우 범용 JacsonJerializer인 GenericJackson2JsonRedisSerializer를 이용한다
// 저장할 클래스가 여러개일 경우 범용 JacksonSerializer인 GenericJackson2JsonRedisSerializer를 이용한다
// 참고 https://somoly.tistory.com/134
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(serializer);
Expand All @@ -82,4 +94,36 @@ public RedisTemplate<String, Object> redisTemplate(ObjectMapper objectMapper) {
return redisTemplate;
}

/**
* Redis Cache를 사용하기 위한 cache manager 등록.<br>
* 커스텀 설정을 적용하기 위해 RedisCacheConfiguration을 먼저 생성한다.<br>
* 이후 RadisCacheManager를 생성할 때 cacheDefaults의 인자로 configuration을 주면 해당 설정이 적용된다.<br>
* RedisCacheConfiguration 설정<br>
* disableCachingNullValues - null값이 캐싱될 수 없도록 설정한다. null값 캐싱이 시도될 경우 에러를 발생시킨다.<br>
* entryTtl - 캐시의 TTL(Time To Live)를 설정한다. Duraction class로 설정할 수 있다.<br>
* serializeKeysWith - 캐시 Key를 직렬화-역직렬화 하는데 사용하는 Pair를 지정한다.<br>
* serializeValuesWith - 캐시 Value를 직렬화-역직렬화 하는데 사용하는 Pair를 지정한다.
* Value는 다양한 자료구조가 올 수 있기 때문에 GenericJackson2JsonRedisSerializer를 사용한다.
*
* @author jun
* @param redisConnectionFactory Redis와의 연결을 담당한다.
* @return
*/
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory,
ObjectMapper objectMapper) {
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig()
.disableCachingNullValues()
.entryTtl(Duration.ofSeconds(defaultExpireSecond))
.serializeKeysWith(
RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)));

return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory)
.cacheDefaults(configuration).build();
}

}
127 changes: 127 additions & 0 deletions src/main/java/com/delfood/controller/LocationController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.delfood.controller;

import com.delfood.aop.OwnerShopCheck;
import com.delfood.controller.reqeust.GetAddressByZipRequest;
import com.delfood.controller.reqeust.GetAddressesByRoadRequest;
import com.delfood.dto.AddressDTO;
import com.delfood.dto.DeliveryLocationDTO;
import com.delfood.service.AddressService;
import com.delfood.service.ShopService;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpSession;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/locations/")
public class LocationController {
@Autowired
private ShopService shopService;

@Autowired
private AddressService addressService;

/**
* 매장의 배달 가능 지역을 추가한다. 배달 가능 지역에 포함되어 있는 사용자에게 검색이 된다. 클라이언트에서는 요청 전 중복된 배달지역이 있는지 체크해야한다. 체크하지 않은
* 상태로 중복된 추가 요청을 보낼 경우 예외처리를 진행한다.
*
* @param shopId 배달 지역을 추가할 매장의 아이디
* @param addDeliveryLocationRequest 추가할 지역 리스트
* @return
*/
@PostMapping("deliveries/{shopId}/possibles")
@OwnerShopCheck
@ResponseStatus(HttpStatus.CREATED)
public void addDeliveryLocation(
@PathVariable(name = "shopId") Long shopId,
@RequestBody(required = true) AddDeliveryLocationRequest addDeliveryLocationRequest) {
Set<String> townCodes = addDeliveryLocationRequest.getTownCodes();
shopService.addDeliveryLocation(shopId, townCodes);
}

/**
* 매장의 배달가능지역을 조회한다.
*
* @author jun
* @param shopId 배달가능 지역을 조회할 매장의 id
* @return
*/
@GetMapping("deliveries/{shopId}/possibles")
@OwnerShopCheck
public List<DeliveryLocationDTO> getDeliveryLocations(
@PathVariable(name = "shopId") Long shopId) {
return shopService.getDeliveryLocations(shopId);
}


/**
* 배달 지역 삭제.
*
* @author jun
* @param deliveryLocationId 삭제할 배달 지역 id
* @param session 접속한 사용자의 세션
* @return
*/
@DeleteMapping("deliveries/{shopId}/possibles/{deliveryLocationId}")
@OwnerShopCheck
public void deleteDeliveryLocation(
@PathVariable(value = "shopId") Long shopId,
@PathVariable(value = "deliveryLocationId") Long deliveryLocationId,
HttpSession session) {
shopService.deleteDeliveryLocation(deliveryLocationId);
}

/**
* 도로명 주소를 검색한다.
*
* @author jun
* @param requestInfo 검색할 도로명 주소 정보.
* @return
*/
@GetMapping("address/road")
public List<AddressDTO> getAddressByRoadInfo(
GetAddressesByRoadRequest requestInfo) {
List<AddressDTO> addresses = addressService.getAddressByRoadName(requestInfo);
return addresses;
}


/**
* 지번 주소를 검색한다.
*
* @author jun
* @param requestInfo 검색할 지번 주소 정보.
* @return
*/
@GetMapping("address/zip")
public List<AddressDTO> getAddressByZipInfo(
GetAddressByZipRequest requestInfo) {
List<AddressDTO> addresses = addressService.getAddressByZipAddress(requestInfo);
return addresses;
}



// ---------------------- Request 객체 ----------------------
@Getter
@Setter
private static class AddDeliveryLocationRequest {
@NonNull
private Set<String> townCodes;
}


}
Loading

0 comments on commit dbdbe37

Please sign in to comment.