- 
                            
                            [Aws + Spring] Spring Boot + Aws Glue + S3๋ฅผ ํ์ฉํ ๋ฐฉ๋ฌธ์ ํต๊ณ ๊ตฌ์ถ (1)Spring 2024. 1. 2. 22:48๋ฐ์ํ reference: https://www.cloudmantra.net/blog/aws-glue-simple-flexible-and-cost-effective-etl/ ๐ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ตฌ์ถํ ๋ ๋ฐฉ๋ฌธ์ ํต๊ณ ๋ฐ ์ค์๊ฐ ๋ฐ์ดํฐ ๋ถ์์ํ์ ์์์ ๋๋ค. 
 ์ด๋ฒ ํฌ์คํ ์์๋ Spring Boot, AWS Glue, ๊ทธ๋ฆฌ๊ณ S3๋ฅผ ๊ฒฐํฉํ์ฌ ๋ฐฉ๋ฌธ์ ํต๊ณ ์์คํ ์
 ๊ตฌ์ถํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด๊ฒ ์ต๋๋ค.
 ์ด ํฌ์คํ ์์๋ ์๋ ๋ ๊ฐ์ง ํต๊ณ๋ฅผ ๊ตฌ์ถํ ์์ ์ ๋๋ค.1. ์ผ์ผ ๋ฐฉ๋ฌธ์ ๋๋ฐ์ด์ค ํต๊ณ (PC, ๋ชจ๋ฐ์ผ, ํ ๋ธ๋ฆฟ ์)
 2. ์ผ์ผ ์ฌ์ดํธ ํต๊ณ (๋ฐฉ๋ฌธ์ ์, ํ์ด์ง ๋ทฐ, ํ์๊ฐ์ ์)
 ๐ฏ ์ด ํฌ์คํธ๋ฅผ ํตํด ์ป์ ์ ์๋ ๊ฒ:- AWS Glue๋ฅผ ์ด์ฉํ ๋ฐ์ดํฐ ETL ํ๋ก์ธ์ค ์๋ํ
- AWS S3๋ฅผ ํ์ฉํ ํ์ฅ ๊ฐ๋ฅํ ํต๊ณ ๋ฐ์ดํฐ ์ ์ฅ์ ๊ตฌ์ถ
- ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ฐฉ๋ฌธ์ ํต๊ณ ํ๋ก์ธ์ค ๊ตฌ์ถ
 โ๏ธ ๋ชฉ์ฐจ1. AWS S3 ๋ก๊ทธ ํด๋ ์์ฑ
 2. Spring Boot ์ค์ 
 3. Spring Boot์์์ ๋ฐฉ๋ฌธ์ ํต๊ณ ์์ง API ๊ฐ๋ฐ
 4. AWS Glue ์์ ๋ฐ ์คํฌ๋ฆฝํธ ์์ฑ์ค์ตํ๊ฒฝ- Intellij Ultimate
- JAVA 17
- Spring Boot 2.7.3
- Spring Security
- Maven
- MySQL
 
 1. AWS S3 ๋ก๊ทธ ํด๋ ์์ฑ๋จผ์  ๋ก๊ทธ ํ์ผ์ ์์ AWS S3์ ๋๋ ํ ๋ฆฌ๋ฅผ ์ถ๊ฐ ํด์ค์ผ ํฉ๋๋ค. 
 S3 ๋ฒํท์ ์์ฑ ๋์ด ์๋ค๋ ์ ์ ํ์ ์งํ ํ๊ฒ ์ต๋๋ค.
 S3 ๋ฒํท ์์ฑ์ ์๋ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํด์ฃผ์ธ์.
 https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/userguide/create-bucket-overview.html
   
 
 ๋ฃจํธ ๋๋ ํ ๋ฆฌ ํ์์ /log/site-visit directory๋ฅผ ์ถ๊ฐํฉ๋๋ค. ์ด๋ ๊ฒ ๋ก๊ทธ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ S3 Bucket์ Directory๋ ์ค๋น ๋์ต๋๋ค.์ผ์ผ ์ฌ์ดํธ ํต๊ณ๋ฅผ ์ํ site-signup, site-page-view directory๋ ์ถ๊ฐ ํด๋๊ฒ ์ต๋๋ค. 
 
 2. Spring Boot ์ค์ ๋จผ์ , AWS์ ๋ก๊ทธ ํ์ผ์ ์๊ธฐ ์ํด์ Aws Client ์ค์ ์ ํด์ค์ผ ํฉ๋๋ค. 
 Maven Dependency์ s3 sdk๋ฅผ ์ถ๊ฐ ํ๊ฒ ์ต๋๋ค.<dependency> <groupId>com.amazonaws</groupId> <artifactId>aws-java-sdk-s3</artifactId> <version>1.12.470</version> </dependency>
 
 
 Aws Client ์ค์ ์ ํ๋ AwsConfig Class๋ฅผ ์์ฑํ๊ณ S3 Bucket ์ ๋ณด์ Credentials ์ ๋ณด๋ฅผ ์ด๊ธฐํ ํฉ๋๋ค.
 ๋จผ์ , ๊ด๋ จ ์ ๋ณด๋ฅผ application.yml์ ์ด๊ธฐํ ํด์ค์ผ ํฉ๋๋ค.
 
 ๐ application.ymlcloud: aws: credentials: access-key: {Your Access Key} secret-access-key: {Your Secret Key} s3: bucket: {Your Bucket Name} region: static: ap-northeast-2
 
 
 
 ํต๊ณ์ฉ ๋ก๊ทธ ๋ฐ์ดํฐ ํ์ผ์ ์์ฑํ๊ณ S3์ ์ ๋ก๋๋ฅผ ํ๋ Method๋ฅผ ์ถ๊ฐ ํ๊ธฐ ์ํด AwsComponent๋ฅผ ์์ฑํฉ๋๋ค.
 AmazonS3 ํด๋์ค์ putObject method๋ก ๋ก๊ทธ ํ์ผ์ ์์ฑํ ์ ์์ต๋๋ค.
 
 ๐ AwsComponent.class@Slf4j @Builder public class AwsComponent { private Environment environment; private String bucketName; private AmazonS3 amazonS3; private String accessKey; private String secretKey; public void saveToFile(String path, String content) { log.info("save file path : {}", path); try { amazonS3.putObject(bucketName, path, content); } catch (AmazonS3Exception e) { log.error("save file error : path[{}], message[{}]", path, e.getMessage()); throw e; } } }
 
 
 
 ์์์ ์์ฑํ AwsComponent์ AwsClient ๋ฐ S3 ์ ๋ณด๋ฅผ ์ด๊ธฐํ ํ๊ธฐ ์ํ AwsConfig๋ฅผ ์์ฑํฉ๋๋ค.
 ์ด๋ ๊ฒ Bean์ ์ถ๊ฐ ํด์ฃผ๋ฉด, AwsComponent๋ฅผ DI ํ ๋ ์ค์  ์ ๋ณด๊ฐ ์ด๊ธฐํ ๋์ด ์๊ฒ ๋ฉ๋๋ค.
 
 ๐ AwsConfig.class@Configuration @RequiredArgsConstructor public class AwsConfig { private final Environment environment; @Value("${cloud.aws.s3.bucket}") private String bucket; @Value("${cloud.aws.credentials.access-key}") private String accessKey; @Value("${cloud.aws.credentials.secret-access-key}") private String secretKey; @Value("${cloud.aws.region.static}") private String region; @Bean public AwsComponent amazonS3Client() { BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); AWSStaticCredentialsProvider staticCredentialsProvider = new AWSStaticCredentialsProvider(awsCredentials); return AwsComponent .builder() .bucketName(bucket) .environment(environment) .accessKey(accessKey) .secretKey(secretKey) .amazonS3( AmazonS3ClientBuilder .standard() .withRegion(region) .enablePathStyleAccess() .withCredentials(staticCredentialsProvider) .build()) .build(); } }
 
 
 ์ด๋ ๊ฒ AWS S3 ๊ด๋ จ ์ค์ ๊ณผ ๋ก๊ทธ ํ์ผ์ ์ ๋ก๋ํ๋ Method ์ถ๊ฐ๊น์ง ์๋ฃ ํ์ต๋๋ค.
 ์ด์  ํต๊ณ ์ ๋ณด๋ฅผ DB์ ์ ์ฅํ๊ธฐ ์ํ ํด๋์ค๋ค์ ์์ฑ ํ๊ฒ ์ต๋๋ค.์ถํ AWS File์ Body์ ์ ๋ ฅํ ํด๋์ค๋ค์ ์์ฑ์๊ฐ ํ๋๋ฅผ ๊ณตํต ํด๋์ค๋ก ์์ฑํฉ๋๋ค. ๐ CommonLog.class @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @MappedSuperclass public abstract class CommonLog { protected LocalDateTime dateTime = LocalDateTime.now(); protected LocalDate date = dateTime.toLocalDate(); }์ผ์ผ ๋ฐฉ๋ฌธ์, ๋๋ฐ์ด์ค Entity์ ์์ฑ์๊ฐ์ ๊ณตํต ํด๋์ค๋ก ์์ฑํฉ๋๋ค. ๐ CommonLog.class @Getter @MappedSuperclass @EntityListeners(AuditingEntityListener.class) public abstract class CommonCreateDateTimeEntity { @Setter @CreatedDate @Column(nullable = false, columnDefinition = "datetime default now()") @Comment("์์ฑ ์ผ์") protected LocalDateTime createdDateTime; }
 ๋จผ์  ์ฌ์ดํธ ๋ก๊ทธ ์ ํ Enum์ ์์ฑํฉ๋๋ค.
 ๐ SiteLogType.classpublic enum SiteLogType { VISIT, PAGE_VIEW, }๋ฐฉ๋ฌธ์ ๋๋ฐ์ด์ค ์ ๋ณด๊ฐ ๋ด๊ฒจ์๋ SiteVisitLog Entity๋ฅผ ์์ฑํฉ๋๋ค. userId๋ ๋ก๊ทธ์ธ ํ ์ฌ์ฉ์์ ๊ณ ์ ํ ์์ด๋ ๊ฐ์ ๋๋ค. 
 ๐ SiteVisitLog.class@Entity @Getter @Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class SiteVisitLog extends CommonLog { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Comment("๋๋ฐ์ด์ค") private String device; @Comment("์ฌ์ฉ์ ์์ด๋") private String userId; private String osFamily; private String osVersion; private String browserFamily; private String browserVersion; @Enumerated(EnumType.STRING) private final SiteLogType type = SiteLogType.VISIT; public SiteVisitLog(String userId) { this.userId = userId; } public SiteVisitLog(String userId, String device, String osFamily, String osVersion, String browserFamily, String browserVersion) { this(userId); this.device = device; this.osFamily = osFamily; this.osVersion = osVersion; this.browserFamily = browserFamily; this.browserVersion = browserVersion; } }๊ฐ ํ๋์ ์ญํ ๊ณผ ์๋ฏธ์ ๋ํ ๊ฐ๋จํ ์ค๋ช ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค: - device (๋๋ฐ์ด์ค):
- ์ค๋ช : ์ฌ์ฉ์๊ฐ ์ก์ธ์คํ๋ ๋๋ฐ์ด์ค์ ์ด๋ฆ์ ๋ํ๋ด๋ ๋ฌธ์์ด์ ๋๋ค. ๋จ, Android ๊ฐ์ ๊ฒฝ์ฐ์ device์ 'android'๊ฐ ์๋, 'Samsung-xxx' ์ ๊ฐ์ ์ด๋ฆ์ด ์ ์ฅ ๋ ์ ์์ต๋๋ค. ์ด๋ด ๊ฒฝ์ฐ์ ์๋์ osFamily๋ฅผ ํ์ธํด์ผ ํฉ๋๋ค.
- ์์: "Windows", "Mac OS", "Linux", "Android", "Samsung-xxx", "IPad" ๋ฑ
 
- osFamily (์ด์์ฒด์  ํจ๋ฐ๋ฆฌ):
- ์ค๋ช : ์ฌ์ฉ์๊ฐ ์ก์ธ์คํ๋ ๋๋ฐ์ด์ค์ ์ด์์ฒด์ ๋ฅผ ๋ํ๋ด๋ ๋ฌธ์์ด์ ๋๋ค.
- ์์: "Windows", "Mac OS", "Linux" ๋ฑ
 
- osVersion (์ด์์ฒด์  ๋ฒ์ ):
- ์ค๋ช : ์ฌ์ฉ์์ ๋๋ฐ์ด์ค์ ์ค์น๋ ์ด์์ฒด์ ์ ๋ฒ์ ์ ๋ํ๋ด๋ ๋ฌธ์์ด์ ๋๋ค.
- ์์: "10.0.19042" (Windows 10์ ๋ฒ์ ), "11.3.1" (macOS Big Sur์ ๋ฒ์ ) ๋ฑ
 
- browserFamily (๋ธ๋ผ์ฐ์  ํจ๋ฐ๋ฆฌ):
- ์ค๋ช : ์ฌ์ฉ์๊ฐ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ก์ธ์คํ๋ ๋ฐ ์ฌ์ฉํ๋ ๋ธ๋ผ์ฐ์ ๋ฅผ ๋ํ๋ด๋ ๋ฌธ์์ด์ ๋๋ค.
- ์์: "Chrome", "Firefox", "Safari" ๋ฑ
 
- browserVersion (๋ธ๋ผ์ฐ์  ๋ฒ์ ):
- ์ค๋ช : ์ฌ์ฉ์๊ฐ ์ฌ์ฉํ๋ ๋ธ๋ผ์ฐ์ ์ ๋ฒ์ ์ ๋ํ๋ด๋ ๋ฌธ์์ด์ ๋๋ค.
- ์์: "94.0.4606.81" (Google Chrome์ ๋ฒ์ ), "92.0.4515.159" (Mozilla Firefox์ ๋ฒ์ ) ๋ฑ
 
 
 ์ผ์ผ ๋ฐฉ๋ฌธ์ ๋๋ฐ์ด์ค ํต๊ณ Entity๋ฅผ ์์ฑํฉ๋๋ค. ๋ฐ์คํฌํ, ๋ชจ๋ฐ์ผ, ํ ๋ธ๋ฆฟ, unknown ๋๋ฐ์ด์ค ์ ๋ณด๋ฅผ ํฌํจํ๊ณ ์์ต๋๋ค.๐ DailyDeviceStats.class @Entity @Getter @NoArgsConstructor public class DailyDeviceStats extends CommonCreateDateTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private LocalDate date; private Long desktopCount; private Long mobileCount; private Long tabletCount; private Long unknownCount; public DailyDeviceStats(LocalDate date) { this(date, 0L, 0L, 0L, 0L); } public DailyDeviceStats(LocalDate date, Long desktopCount, Long mobileCount, Long tabletCount, Long unknownCount) { this.date = date; this.desktopCount = desktopCount; this.mobileCount = mobileCount; this.tabletCount = tabletCount; this.unknownCount = unknownCount; } }์ผ์ผ ์ฌ์ดํธ ํต๊ณ Entity๋ฅผ ์์ฑํฉ๋๋ค. ๋ฐฉ๋ฌธ์, ํ์๊ฐ์ , ํ์ด์ง๋ทฐ(PV) ์ ์ ๋ณด๋ฅผ ํฌํจํ๊ณ ์์ต๋๋ค. ๐ DailySiteStats.class @Entity @Getter @Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(uniqueConstraints = { @UniqueConstraint(columnNames = {"date"}, name = "daily_site_stats_uk") }) public class DailySiteStats extends CommonCreateDateTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private LocalDate date; private Long visitorCount; private Long signUpCount; private Long pageViewCount; public DailySiteStats(LocalDate date) { this(date, 0L, 0L, 0L); } public DailySiteStats(LocalDate date, Long visitorCount, Long signUpCount, Long pageViewCount) { this.date = date; this.visitorCount = visitorCount; this.signUpCount = signUpCount; this.pageViewCount = pageViewCount; } public void addVisitorCount() { this.visitorCount++; } public void addSignUpCount() { this.signUpCount++; } public void addPageViewCount() { this.pageViewCount++; } }์ผ๋ณ ์ค๋ณต ๋ฐ์ดํฐ๊ฐ ์์ฑ ๋๋๊ฒ์ ๋ฐฉ์ง ํ๊ธฐ ์ํด์ @UniqueConstraint ์ ์ฝ์กฐ๊ฑด์ ์ค์  ํ์ต๋๋ค. ๋ฐ์ดํฐ ์ ์ฅ์ Lock์ ์ค์ ํด์ ๋์์ฑ ์ ์ด๋ฅผ ์ฒ๋ฆฌํ ์๋ ์์ง๋ง, Lock์ผ๋ก ์ธํ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ์๋๊ฐ ์ง์ฐ ๋๋ฉด ์๋๋ ๋ฐ์ดํฐ์ด๊ธฐ ๋๋ฌธ์, Unique Key๋ฅผ ์ค์ ํ์์ต๋๋ค. S3์ body๋ฅผ write ํ๊ธฐ ์ํ ํด๋์ค๋ค์ ์ถ๊ฐ ํ๊ฒ ์ต๋๋ค. ์๋ ํด๋์ค๋ค์ ์ ์ฅ ํ๊ธฐ ์ํ ์ฉ๋๊ฐ ์๋๋ผ์ Entity๊ฐ ์๋๋๋ค. ๐ PageViewLog.class @Getter @Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class PageViewLog extends CommonLog { private String userId; private String uri; private final SiteLogType type = SiteLogType.PAGE_VIEW; public PageViewLog(String userId, String uri) { this.userId = userId; this.uri = uri; } }๐ SiteSignUpLog.class @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter public class SiteSignUpLog extends CommonLog { private String userId; private final SiteLogType type = SiteLogType.SIGN_UP; public SiteSignUpLog(String userId) { this.userId = userId; } }์์์ ์ถ๊ฐํ ๋ ์ง ํ๋๊ฐ ์ ์ธ ๋์ด ์๋ CommonLog ํด๋์ค๋ฅผ ์์ ๋ฐ์์ต๋๋ค. 
 ๋ง์ฐฌ๊ฐ์ง๋ก SiteLogType Enum Class๋ก ์ ํ์ ๊ตฌ๋ถํฉ๋๋ค.์ด๋ก์จ, ํ์ํ ๊ธฐ๋ณธ์ ์ธ ํด๋์ค๋ค์ ์์ฑ์ ํด์คฌ์ต๋๋ค. ๋จผ์  ์ฌ์ฉ์์ Device ์ ๋ณด๋ฅผ ์๊ธฐ ์ํด์ Interceptor๋ฅผ ์์ฑํ๊ฒ ์ต๋๋ค. 
 3. Spring Boot์์์ ๋ฐฉ๋ฌธ์ ํต๊ณ ์์ง API ๊ฐ๋ฐUserAccessInterceptor ์์๋ API ํธ์ถ์ด ๋ฐ์ํ ๋๋ง๋ค preHandle method๊ฐ ์คํ๋ฉ๋๋ค. ์ฌ์ฉ์์ ๋ฐฉ๋ฌธ์ผ์ ์ธ์ ์ ์ ์ฅ ํ ๋ค, ์ผ๋ณ๋ก ์ต์ด 1๋ฒ๋ง ์คํํ๊ธฐ ์ํด์ ์ธ์ ์ ๋ฐฉ๋ฌธ์ผ์ด ์๊ฑฐ๋ ์ ์ฅ ๋ ๋ฐฉ๋ฌธ์ผ๊ณผ ๋ค๋ฅธ ๋ ์ง(์ด์  ๋ )์ธ ๊ฒฝ์ฐ, 
 ๋ฐฉ๋ฌธ์์ ๋๋ฐ์ด์ค ์ ๋ณด๋ฅผ DB์ S3์ ์ ์ฅํฉ๋๋ค.๐ CustomInterceptor.class interceptor๊ฐ ์ฌ๋ฌ๊ฐ์ธ ๊ฒฝ์ฐ๋ฅผ ์ํด HandlerInterceptor๋ฅผ ์์ ๋ฐ์ interface๋ฅผ ์์ฑํฉ๋๋ค. 
 ์ดํ InterceptorRegistry์ pathPattern๋ฅผ ์ถ๊ฐํ๊ธฐ ์ํด pathPattern ๋ฉ์๋๋ ์ ์ธํฉ๋๋ค.public interface CustomInterceptor extends HandlerInterceptor { String pathPattern(); }๐ UserAccessInterceptor.class ์ด ํด๋์ค๋ CustomInterceptor๋ฅผ ๊ตฌํํ๊ณ ์์ผ๋ฉฐ, Spring์ HandlerInterceptor๋ฅผ ํ์ฅํ์ฌ API ์๋ํฌ์ธํธ์ ์ ๊ทผํ ์ฌ์ฉ์์ ๋๋ฐ์ด์ค ์ ๋ณด๋ฅผ ๋ก๊น ํ๋ ์ญํ ์ ํฉ๋๋ค. @Component public class UserAccessInterceptor implements CustomInterceptor { @Autowired private LoggingWebService loggingWebService; private final Parser uaParser = new Parser(); private static final GrantedAuthority USER_AUTHORITY = ROLE_AUTHORITIES_MAP.get(UserRole.NORMAL); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(false); if (session == null) return true; Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Object principal = authentication.getPrincipal(); if (principal instanceof SessionUser sessionUser) { if (!sessionUser.getAuthorities().contains(USER_AUTHORITY)) return true; // ์ฌ์ฉ์์ ๋ง์ง๋ง ๋ก๊ทธ์ธ ์๊ฐ์ ์ ์ฅํด๋๊ณ , ํ์ฌ ์๊ฐ๊ณผ ๋น๊ตํ์ฌ ๋ค๋ฅด๋ฉด ๋ก๊ทธ๋ฅผ ๋จ๊ธด๋ค. Object lastLoginDate = session.getAttribute("lastLoginDate"); LocalDate today = LocalDate.now(); if (lastLoginDate == null || !((LocalDate)lastLoginDate).isEqual(today)) { session.setAttribute("lastLoginDate", today); String userAgent = defaultString(request.getHeader("User-Agent"), ""); logDeviceInfo(sessionUser.getId(), userAgent); } } return true; } @Override public String pathPattern() { return "/api/**"; } private void logDeviceInfo(String userId, String userAgent) { Client client = uaParser.parse(userAgent); String deviceFamily = client.device.family; String osFamily = client.os.family; String osVersion = client.os.major + "." + client.os.minor; String browserFamily = client.userAgent.family; String browserVersion = client.userAgent.major + "." + client.userAgent.minor; loggingWebService.addSiteVisitLog(userId, deviceFamily, osFamily, osVersion, browserFamily, browserVersion); } }UserAccessInterceptor์ ๋ํ ์ค๋ช ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค. - UserAgent ํ์ฑ:
- User-Agent ์ ๋ณด๋ฅผ ํ์ฑํ๊ธฐ ์ํ uaParser ์ธ์คํด์ค๋ฅผ ์์ฑํฉ๋๋ค.
 
- ์ฌ์ฉ์ ๊ถํ ๋ฐ ๋ก๊ทธ์ธ ์๊ฐ ๋ก๊น
:
- preHandle ๋ฉ์๋์์๋ ํ์ฌ ์ฌ์ฉ์์ ์ธ์  ์ ๋ณด๋ฅผ ํ์ธํ๊ณ , ํด๋น ์ฌ์ฉ์๊ฐ ์ผ๋ฐ ์ฌ์ฉ์ ๊ถํ์ ๊ฐ์ง๊ณ ์๋ ๊ฒฝ์ฐ(UserRole.NORMAL), ๋ง์ง๋ง ๋ก๊ทธ์ธ ์๊ฐ์ ํ์ธํ์ฌ ๋ค๋ฅธ ๋ ์ง์ธ ๊ฒฝ์ฐ์๋ง ๋ก๊ทธ์ธ ๊ธฐ๊ธฐ ์ ๋ณด๋ฅผ ๋ก๊น ํฉ๋๋ค.
- ๋ก๊ทธ์ธ ์๊ฐ์ ์ธ์ ์ ์์ฑ์ผ๋ก ์ ์ฅ๋์ด ๋ค์ ๋ก๊ทธ์ธ ์ ๋น๊ต์ ์ฌ์ฉ๋ฉ๋๋ค.
 
- ๋ก๊ทธ์ธ ๊ธฐ๊ธฐ ์ ๋ณด ๋ก๊น
:
- logDeviceInfo ๋ฉ์๋์์๋ User-Agent ์ ๋ณด๋ฅผ ํ์ฑํ๊ณ , ํด๋น ์ ๋ณด๋ฅผ ์ด์ฉํ์ฌ ๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ๊ธฐ๊ธฐ ์ ๋ณด๋ฅผ ์ถ์ถํฉ๋๋ค.
- ์ถ์ถ๋ ์ ๋ณด๋ loggingWebService๋ฅผ ํตํด ๋ก๊ทธ๋ก ๊ธฐ๋ก๋ฉ๋๋ค.
 
- ์ ์ฉ ๊ฒฝ๋ก:
- pathPattern ๋ฉ์๋์์๋ ์ด ์ธํฐ์ ํฐ๊ฐ ์ด๋ค ๊ฒฝ๋ก์ ์ ์ฉ๋๋์ง๋ฅผ ๋ํ๋ด๋ ํจํด์ ์ง์ ํฉ๋๋ค. ํ์ฌ๋ /api/** ํจํด์ผ๋ก ์ค์ ๋์ด API ์๋ํฌ์ธํธ์๋ง ์ ์ฉ๋ฉ๋๋ค.
 
 ํด๋น interceptor๋ฅผ ๊ตฌํ ํ๋ค๊ณ ์คํ ๋๋๊ฒ์ด ์๋๋ผ, InterceptorRegistry์ interceptor๋ฅผ ์ถ๊ฐํด์ค์ผ ํฉ๋๋ค. ๐ WebConfig.class @Configuration @RequiredArgsConstructor public class WebConfig implements WebMvcConfigurer { private final Environment environment; private final List<CustomInterceptor> interceptors; @Override public void addInterceptors(InterceptorRegistry registry) { for (CustomInterceptor interceptor : interceptors) { registry.addInterceptor(interceptor) .addPathPatterns(interceptor.pathPattern()); } } }CustomInterceptor Interface ๋ชฉ๋ก์ DI ํ ๋ค, addInterceptors override method์ pathPattern๊ณผ ํจ๊ป interceptor๋ฅผ ์ถ๊ฐํฉ๋๋ค. ์ด์  "/api/**" ๊ฒฝ๋ก์ API๊ฐ ํธ์ถ ๋ ๋๋ง๋ค UserAccessInterceptor์ prehandle method๊ฐ ์คํ๋ฉ๋๋ค. ๋ด์ฉ์ด ๊ธธ์ด์ง๋ ๊ด๊ณ๋ก ๋ค์ ํฌ์คํ ์์ ์ด์ด์ ์์ฑํ๊ฒ ์ต๋๋ค. 
 ๋ฐ์ํ'Spring' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ