ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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.yml

    cloud:
      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.class

    public 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;
        }
    }

     

    ๊ฐ ํ•„๋“œ์˜ ์—ญํ• ๊ณผ ์˜๋ฏธ์— ๋Œ€ํ•œ ๊ฐ„๋‹จํ•œ ์„ค๋ช…์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

    1. device (๋””๋ฐ”์ด์Šค):
      • ์„ค๋ช…: ์‚ฌ์šฉ์ž๊ฐ€ ์•ก์„ธ์Šคํ•˜๋Š” ๋””๋ฐ”์ด์Šค์˜ ์ด๋ฆ„์„ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฌธ์ž์—ด์ž…๋‹ˆ๋‹ค. ๋‹จ, Android ๊ฐ™์€ ๊ฒฝ์šฐ์—” device์— 'android'๊ฐ€ ์•„๋‹Œ,  'Samsung-xxx' ์™€ ๊ฐ™์€ ์ด๋ฆ„์ด ์ €์žฅ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿด ๊ฒฝ์šฐ์—” ์•„๋ž˜์˜ osFamily๋ฅผ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
      • ์˜ˆ์‹œ: "Windows", "Mac OS", "Linux", "Android", "Samsung-xxx", "IPad" ๋“ฑ
    2. osFamily (์šด์˜์ฒด์ œ ํŒจ๋ฐ€๋ฆฌ):
      • ์„ค๋ช…: ์‚ฌ์šฉ์ž๊ฐ€ ์•ก์„ธ์Šคํ•˜๋Š” ๋””๋ฐ”์ด์Šค์˜ ์šด์˜์ฒด์ œ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฌธ์ž์—ด์ž…๋‹ˆ๋‹ค.
      • ์˜ˆ์‹œ: "Windows", "Mac OS", "Linux" ๋“ฑ
    3. osVersion (์šด์˜์ฒด์ œ ๋ฒ„์ „):
      • ์„ค๋ช…: ์‚ฌ์šฉ์ž์˜ ๋””๋ฐ”์ด์Šค์— ์„ค์น˜๋œ ์šด์˜์ฒด์ œ์˜ ๋ฒ„์ „์„ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฌธ์ž์—ด์ž…๋‹ˆ๋‹ค.
      • ์˜ˆ์‹œ: "10.0.19042" (Windows 10์˜ ๋ฒ„์ „), "11.3.1" (macOS Big Sur์˜ ๋ฒ„์ „) ๋“ฑ
    4. browserFamily (๋ธŒ๋ผ์šฐ์ € ํŒจ๋ฐ€๋ฆฌ):
      • ์„ค๋ช…: ์‚ฌ์šฉ์ž๊ฐ€ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์•ก์„ธ์Šคํ•˜๋Š” ๋ฐ ์‚ฌ์šฉํ•˜๋Š” ๋ธŒ๋ผ์šฐ์ €๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฌธ์ž์—ด์ž…๋‹ˆ๋‹ค.
      • ์˜ˆ์‹œ: "Chrome", "Firefox", "Safari" ๋“ฑ
    5. 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์— ๋Œ€ํ•œ ์„ค๋ช…์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

     

    1. UserAgent ํŒŒ์‹ฑ:
      • User-Agent ์ •๋ณด๋ฅผ ํŒŒ์‹ฑํ•˜๊ธฐ ์œ„ํ•œ uaParser ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
    2. ์‚ฌ์šฉ์ž ๊ถŒํ•œ ๋ฐ ๋กœ๊ทธ์ธ ์‹œ๊ฐ„ ๋กœ๊น…:
      • preHandle ๋ฉ”์„œ๋“œ์—์„œ๋Š” ํ˜„์žฌ ์‚ฌ์šฉ์ž์˜ ์„ธ์…˜ ์ •๋ณด๋ฅผ ํ™•์ธํ•˜๊ณ , ํ•ด๋‹น ์‚ฌ์šฉ์ž๊ฐ€ ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž ๊ถŒํ•œ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ๊ฒฝ์šฐ(UserRole.NORMAL), ๋งˆ์ง€๋ง‰ ๋กœ๊ทธ์ธ ์‹œ๊ฐ„์„ ํ™•์ธํ•˜์—ฌ ๋‹ค๋ฅธ ๋‚ ์งœ์ธ ๊ฒฝ์šฐ์—๋งŒ ๋กœ๊ทธ์ธ ๊ธฐ๊ธฐ ์ •๋ณด๋ฅผ ๋กœ๊น…ํ•ฉ๋‹ˆ๋‹ค.
      • ๋กœ๊ทธ์ธ ์‹œ๊ฐ„์€ ์„ธ์…˜์˜ ์†์„ฑ์œผ๋กœ ์ €์žฅ๋˜์–ด ๋‹ค์Œ ๋กœ๊ทธ์ธ ์‹œ ๋น„๊ต์— ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
    3. ๋กœ๊ทธ์ธ ๊ธฐ๊ธฐ ์ •๋ณด ๋กœ๊น…:
      • logDeviceInfo ๋ฉ”์„œ๋“œ์—์„œ๋Š” User-Agent ์ •๋ณด๋ฅผ ํŒŒ์‹ฑํ•˜๊ณ , ํ•ด๋‹น ์ •๋ณด๋ฅผ ์ด์šฉํ•˜์—ฌ ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž์˜ ๊ธฐ๊ธฐ ์ •๋ณด๋ฅผ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค.
      • ์ถ”์ถœ๋œ ์ •๋ณด๋Š” loggingWebService๋ฅผ ํ†ตํ•ด ๋กœ๊ทธ๋กœ ๊ธฐ๋ก๋ฉ๋‹ˆ๋‹ค.
    4. ์ ์šฉ ๊ฒฝ๋กœ:
      • 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๊ฐ€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

     

     

    ๋‚ด์šฉ์ด ๊ธธ์–ด์ง€๋Š” ๊ด€๊ณ„๋กœ ๋‹ค์Œ ํฌ์ŠคํŒ…์—์„œ ์ด์–ด์„œ ์ž‘์„ฑํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.


     

    ๋ฐ˜์‘ํ˜•

    ๋Œ“๊ธ€

Designed by Tistory.