컴포넌트 의미

 

컴포넌트(component)란 여러 개의 프로그램 함수들을 모아 하나의 특정한 기능을 수행할 수 있도록 구성한 작은 기능적 단위를 말한다. 컴포넌트를 이용하면 소프트웨어 개발을 마치 레고(Lego) 블록을 쌓듯이 조립식으로 쉽게 할 수 있다. 모듈(module)이라고도 한다. 상용 컴포넌트에는 DextUpload, r-Mate Chart 등이 있다. 컴포넌트 기반의 개발 방법론을 CBD 방법론이라고 한다.

소프트웨어는 독립적으로 개발되지 않은 경우가 많고, 독립적으로 개발되어도 다른 모듈과의 호환을 생각하지 않고 개발한다. 이는 소프트웨어의 재사용을 어렵게 하고 유지보수 비용이 크게 증가하는 원인이 된다. 이러한 상황에서 소프트웨어의 재사용의 중요성과 필요성을 위해 나온 기술이 컴포넌트 기술이다.

 


 

기본 흐름과 주요 컴포넌트

 

스프링 MVC를 이용해서 서비스를 개발하려면 스프링 MVC가 어떤 식으로 동작하는지 이해하고 있어야 합니다.

기본적인 이해없이 개발하게 된다면 여러 응용 단계에서 더 나은 방안을 생각하기 어렵습니다.

이러한 문제를 해결하기 위해 기본 흐름과 주요 컴포넌트에 대해 알고 있을 필요가 있습니다.

 

최초에 사용자가 웹으로 요청을 하게되면 DispatcherServlet이 요청을 받으며, 밑의 이미지와 같은 과정을 거쳐 사용자 웹으로 응답을 전송하게 됩니다.

 

 

*순서

1. 요청전송

2. 요청 URL과 매칭 되는 컨트롤러 검색

3. 처리 요청

4. 실행

5. 결과

6. 컨트롤러 실행 결과를 ModelAndView로 리턴

7. 컨트롤러의 실행 결과를 보여줄 View 검색

8. 응답 생성 요청

9. 응답 생성

 


 

*DispatcherServlet

클라이언트의 요청을 전달받습니다. 컨트롤러에게 클라이언트의 요청을 전달하고, 컨트롤러가 리턴한 결괏값을 View에 전달하여 알맞은 응답을 생성하도록 합니다.

*HandlerMapping

클라이언트의 요청 URL을 어떤 Controller가 처리할지를 결정합니다.

*HandlerAdapter

DispatcherServlet의 처리 요청을 변환해서 Controller에게 전달하고, Controller의 응답 결과를 DispatcherServlet이 요구하는 형식으로 변환합니다.

웹브라우저 캐시 등의 설정도 담당합니다.

*Controller

클라이언트의 요청을 처리한 뒤, 결과를 리턴합니다.

응답 결과에서 보여줄 데이터를 모델에 담아 전달합니다.

*ModelAndView 

Controller가 처리한 결과를 정보 및 뷰 선택에 필요한 정보를 담습니다.

*ViewResolver

Controller의 처리 결과를 보여줄 뷰를 결정합니다.

*View

Controller의 처리 결과 화면을 생성합니다.

JSP나 Velocity 템플릿 파일 등을 이용해서 클라이언트에 응답 결과를 전송합니다.

 

참조자료 : 웹 개발자를 위한 SPRING 4.0 프로그래밍

 


 

후기

 

스프링을 사용할 때 어떤 흐름으로 요청과 응답이 나오는지 모른다면 응용하기 힘들 것입니다.

요즘에는 스프링 부트를 사용하는데, 스프링 부트 장점 중 하나는 개발자가 기본적인 설정에 덜 신경 써도 된다는 것입니다.

그러다 보니 스프링 부트로 프로젝트를 만들고 요청받을 컨트롤러에 경로를 적어주면 만들어 놓은 비즈니스 로직대로 흘러간다 정도만 알 수도 있습니다.

하지만 명확하게 어떤 방식으로 동작하는지 알고 있어야 응용할 때 더 나은 방법으로 접근할 수 있습니다. 

참조 자료: 웹 개발자를 위한 Spring 4.0 프로그래밍

'스프링 > Spring' 카테고리의 다른 글

[Spring] POJO란 무엇인가?  (0) 2022.01.17
[Spring] 스프링 DI란?(Dependency Injection)  (0) 2022.01.14
[스프링] 트랜잭션이란?  (0) 2022.01.12
[Spring] 스프링이란 무엇인가?  (0) 2022.01.10
관점 지향 프로그래밍 의미

 

컴퓨팅에서 관점 지향 프로그래밍(aspect-oriented programming, AOP)은 횡단 관심사(cross-cutting concern)의 분리를 허용함으로써 모듈성을 증가시키는 것이 목적인 프로그래밍 패러다임이다. 

코드 그 자체를 수정하지 않는 대신 기존의 코드에 추가 동작(어드바이스)을 추가함으로써 수행하며, "함수의 이름이 'set'으로 시작하면 모든 함수 호출을 기록한다"와 같이 어느 코드가 포인트 컷(pointcut) 사양을 통해 수정되는지를 따로 지정한다. 

이를 통해 기능의 코드 핵심부를 어수선하게 채우지 않고도 비즈니스 로직에 핵심적이지 않은 동작들을 프로그램에 추가할 수 있게 한다. 

관점 지향 프로그래밍은 관점 지향 소프트웨어 개발의 토대를 형성한다.

 

https://ko.wikipedia.org/wiki/%EA%B4%80%EC%A0%90_%EC%A7%80%ED%96%A5_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D

 

관점 지향 프로그래밍 - 위키백과, 우리 모두의 백과사전

컴퓨팅에서 관점 지향 프로그래밍(aspect-oriented programming, AOP)은 횡단 관심사(cross-cutting concern)의 분리를 허용함으로써 모듈성을 증가시키는 것이 목적인 프로그래밍 패러다임이다. 코드 그 자체

ko.wikipedia.org

 


 

이슈

 

서비스를 개발하면서 다양한 공통 기능을 필요로 하게된다.

대표적으로 로깅, 보안, 트랜잭션을 프로젝트 전반에 걸쳐 적용되는 공통 기능이 존재합니다.

해당 기능들은 특정 메서드에만 필요한 기술이 아니고, 핵심 비즈니스 로직과는 구분되는 기능입니다.

그래서 이 공통 관심 사항을 별도로 분리하기로 했습니다.

 

 

만약 시간 체크 메서드가 서비스 전체를 해야 한다면? (예를 들어 1000개)

복사/붙여 넣기를 이용하더라도 모든 메서드에 시간 체크 기능을 만든다면 많은 시간이 소요될 것입니다.

그래도 복사/붙여넣기를 이용해서 겨우 모든 메서드에 시간체크 기능을 만들었는데

시간 체크 확인 후 모든 시간체크 기능을 제거해야 한다면 다시 많은 시간을 사용하여 제거작업을 해야할 것입니다.

 

이러한 시간체크 기능을 공통으로 관리를 한다면 많은 시간을 사용하지 않아도 되고, 코드도 보다 깔끔해집니다.

개발자는 핵심 비즈니스 로직에 집중할 수 있게 됩니다.

 

AOP 적용 전

 

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cg.park</groupId>
    <artifactId>aop-test</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>aop-test</name>
    <description>aop-test</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

 

ApiController

package cg.park.aoptest.controllers;

import cg.park.aoptest.service.impl.ApiServiceImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StopWatch;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ApiController {

    Logger logger = LoggerFactory.getLogger(this.getClass());
    @Autowired
    ApiServiceImpl apiServiceImpl;

    @RequestMapping("/test")
    public void apiTest() {

        try {
            StopWatch stopWatchA = new StopWatch();
            stopWatchA.start();
            apiServiceImpl.serviceA();
            stopWatchA.stop();

            StopWatch stopWatchB = new StopWatch();
            stopWatchB.start();
            apiServiceImpl.serviceB();
            stopWatchB.stop();

            logger.info("서비스: {}, 소요시간: {}","serviceA",stopWatchA.getTotalTimeMillis());
            logger.info("서비스: {}, 소요시간: {}","serviceB",stopWatchB.getTotalTimeMillis());
        }
        catch (Exception e) {
            logger.error("오류");
        }

    }

}

 

ApiServiceImpl

package cg.park.aoptest.service.impl;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class ApiServiceImpl {

    Logger logger = LoggerFactory.getLogger(this.getClass());

    public void serviceA () throws Exception {
        logger.info("serviceA 실행");
        Thread.sleep(1000);
    }

    public void serviceB () throws Exception {
        logger.info("serviceB 실행");
        Thread.sleep(2000);
    }

}

 

 

기존에는 각 서비스의 실행 시간을 체크할 경우 수작업으로 모든 서비스에 체크 기능을 만들었습니다.

위의 ApiController를 보면 

serviceA의 시간을 체크하고 serviceB의 시간을 체크하는 로직이 각각 구현되어 있습니다.

 


 

AOP 적용

 

ApiController (수정)

package cg.park.aoptest.controllers;

import cg.park.aoptest.service.impl.ApiServiceImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StopWatch;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ApiController {

    Logger logger = LoggerFactory.getLogger(this.getClass());
    @Autowired
    ApiServiceImpl apiServiceImpl;

    @RequestMapping("/test")
    public void apiTest() {

        try {
            apiServiceImpl.serviceA();
            apiServiceImpl.serviceB();
        }
        catch (Exception e) {
            logger.error("오류");
        }

    }

}

 

ApiServiceImpl (수정)

package cg.park.aoptest.service.impl;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class ApiServiceImpl {

    Logger logger = LoggerFactory.getLogger(this.getClass());

    public void serviceA () throws Exception {
        Thread.sleep(1000);
    }

    public void serviceB () throws Exception {
        Thread.sleep(2000);
    }

}

 

AopConfig (신규)

package cg.park.aoptest.comm;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;

@Aspect
@Component
public class AopConfig {

    Logger logger = LoggerFactory.getLogger(this.getClass());

    @Around("execution(* cg.park.aoptest..impl.*.*(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {

        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        joinPoint.proceed();
        stopWatch.stop();

        logger.info("서비스: {}, 소요시간: {}",joinPoint.getSignature().toShortString(), stopWatch.getTotalTimeMillis());
        return joinPoint.proceed();
    }

}

 

 

코드 전/후 비교

왼쪽: Before  |  오른쪽: After

 

왼쪽 코드보다 오른쪽 코드가 깔끔하지 않나요?

그리고 매번 시간 체크 기능을 만들 필요가 없으니 비즈니스 기능 개발에만 집중할 수가 있습니다.

 


 

후기

 

AOP는 Spring의 핵심 기능이라고 생각합니다.

현재 작성된 글은 시간체크 기능이지만 로깅, 트랜잭션, 시큐리티 기능을 적용하게 된다면 필수 기능입니다.

서비스를 개발하고 운영하면 AOP의 필요성이 느껴질 때가 있습니다.

하지만 AOP를 사용해보지 않고 어렵다고 느낄 수 있기에 간편한 예제를 만들어봤습니다.

감사합니다.

 

Git : https://github.com/qkrcksrbs8/aop-test

Spring Cloud Config 의미

 

Spring Cloud Config는 분산 시스템에서 외부화된 구성에 대한 서버 측 미 클라이언트 측 지원을 제공합니다.

Config 서버를 사용하면 모든 환경에서 애플리케이션의 외부 속성을 중앙에서 관리할 수 있습니다.

클라이언트와 서버의 개념은 모두 Spring Environment 및 PropertySource 추상화와 동일하게 매핑되므로 Spring 응용 프로그램과 매우 잘 맞지만 모든 언어로 실행되는 응용 프로그램에서 사용할 수 있습니다.

애플리케이션이 개발에서 테스트로, 운영으로 배포 파이프라인을 통해 이동할 때 이러한 환경 간의 구성을 관리하고 마이그레이션 시 애플리케이션이 실행하는 데 필요한 모든 사항을 갖추도록 할 수 있습니다.

서버 스토리지 백엔드의 기본 구현은 git을 사용하므로 레이블링된 버전의 구성 환경을 쉽게 지원할 뿐만 아니라 컨텐츠 관리를 위한 다양한 툴링에 액세스할 수 있습니다.

대체 구현을 추가하고 Spring 구성을 통해 간편하게 연결할 수 있습니다.

 

https://cloud.spring.io/spring-cloud-config/reference/html/

 

Spring Cloud Config

Many source code repository providers (such as Github, Gitlab, Gitea, Gitee, Gogs, or Bitbucket) notify you of changes in a repository through a webhook. You can configure the webhook through the provider’s user interface as a URL and a set of events in

cloud.spring.io

 


 

이슈

 

서비스를 개발하고 운영을 하다보면 설정 값을 변경해야 하는 경우가 있다.

이 경우에 설정 값을 변경 후 빌드 후 배포하는 과정이 필요하다.

빌드 후 배포하는 과정에서 서버를 재기동해야한다.

Spring Cloud Config는 서버 재기동 없이 설정 값 변경이 가능하다.

 

인프라

 

 

Admin : 관리자 클라이언트

Main Server : Spring Cloud Config Client가 될 곳이다. 서비스 서버이다.

SCC Server : Spring Cloud Config Server이다. Repository 정보를 받아서 Client에 전달해준다.

Repository : 설정 값을 관리할 저장소이다. Git이 될 수도 있고, 로컬에서 관리할 수도 있다.

 

현재는 Admin에서 요청하면 Main Server를 거쳐서 SCC Server로 요청되고 Repository를 조회한 다음 반환된다.

 

구현 Spring Cloud Config Server

 

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.6</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cg.park</groupId>
    <artifactId>config-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>config-server</name>
    <description>config-server</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>2020.0.4</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

 

application.yml

server:
  port: 9100 #SCC Server의 포트설정

spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/qkrcksrbs8/project-config #Repository 주소.

위의 Repository 주소는 본인 로컬경로나 본인 깃주소로 사용하길 바랍니다.

 

ConfigServerApplication

package cg.park.configserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }

}

 

 

구현 Spring Cloud Config Client

 

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cg.park</groupId>
    <artifactId>config-client</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>config-client</name>
    <description>config-client</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>2021.0.0</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

 

application.yml

server:
  port: 9000
spring:
  profiles:
    active: local
  config:
    import: "configserver:http://localhost:9100" # config server url
  cloud:
    config:
      name: config # config name
      profile: local # config sub name
      label: main # branch name
management:
  endpoints:
    web:
      exposure:
        include: refresh # refresh

 

ConfigClientApplication

package cg.park.configclient;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ConfigClientApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConfigClientApplication.class, args);
    }

}

 

ApiController

package cg.park.configclient.controllers;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ApiController {

    Logger logger = LoggerFactory.getLogger(this.getClass());

    @RequestMapping("/test")
    public String apiTest() {

        logger.info("### info !!!");
        logger.debug("### debug !!!");
        return "test입니다.";
    }

}

 

설명

 

서비스를 운영하다 보면 INFO 레벨로 자세한 로그 확인이 불가능하여

DEBUG로 로그레벨 변경 후 로그를 확인하는 경우가 있다.

하지만 로그레벨 변경 후 빌드를 하고 배포를 해야하기 때문에 서비스 운영에 문제가 있다.

 

Spring Cloud Config를 사용하면 빌드 후 재배포 과정없이 로그레벨 변경 가능하다.

 

Admin(관리자) 측이 설정 파일이 있는 Repository의 설정 값을 수정한다. (본인은 Git으로 했음. 로컬도 가능)

 

info 설정으로 되어있는 로그레벨을 debug로 수정.

 

Spring Cloud Config의 Refresh 기능으로 설정 값을 갱신한다.

 

PostMan을 이용하여 Refresh 사용

http://localhost:9000/actuator/refresh

 

로그레벨이 info일 때는 debug는 나오지 않음.

 

로그레벨을 debug로 바꾼 후에는 debug까지 나옴.

 

위의 과정은 서버 재기동없이 진행되었습니다.

 

깃 주소

 

Client : https://github.com/qkrcksrbs8/config-client2

Server : https://github.com/qkrcksrbs8/config-server


 

후기

 

Spring Cloud Config를 보고 처음에는 설정파일을 프로젝트로부터 분리시켜 관리를 용이하게 만든다. 라는 정보만 알고있었다.

위의 내용만으로는 굳이 지금 서비스에 적용할 이유가 없다.

하지만 현재 Spring Cloud Config 기술이 필요한 부분이 발생하여 적용하게 되었다.

위의 내용처럼 Log 레벨 변경 시 서버를 재기동하는 과정이 매우 불편했기 때문이다.

지금 예시로 보여주는 기능은 Log 레벨 변경이지만, 응용을 한다면 properties에 들어가는 모든 설정 값들을 외부파일로 분리시켜서 관리할 수 있다.

 

 

 

 

링크

 

https://www.krcert.or.kr/data/secNoticeView.do?bulletin_writing_sequence=36397 

 

KISA 인터넷 보호나라&KrCERT

KISA 인터넷 보호나라&KrCERT

www.boho.or.kr

 


 

이슈

 

Log4j 2에서 발생하는 취약점에 대해 Apache 소프트웨어 재단이 보안 업데이트를 권고하였습니다.

이는 보안 위협 수준 1~10단계 중 최고 단계인 10단계에 해당한다고 설명하고 있습니다.

공격자는 해당 취약점을 이용하여 정상 서비스 중지 등의 피해를 발생시킬 수 있으므로, 최신 버전으로 업데이트를 권고하였습니다.

JNDI 정보와 LDAP의 정보를 알 수 있다고 합니다.

 

방안

 

 - Java 8 이상 : Log4j 2.17.1으로 업데이트
 - Java 7 : Log4j 2.12.4으로 업데이트[5] - Java 6 : Log4j 2.3.2으로 업데이트[5]

※ log4j-core-*.jar 파일 없이 log4j-api-*.jar 파일만 사용하는 경우 위 취약점의 영향을 받지 않음

 

후기

 

현재 온라인상에 나와있는 대처방법으로는 Apache Log4j 보안 업데이트 권고에 있는 취약점 버전을 사용하지 않고, 신규 버전을 사용하는 방법이 있습니다.

이슈가 되는 내용은 해커들의 공격으로 JNDI 정보와 LDAP 정보를 알아낼 수 있다고 합니다. 

현재 취약점 버전을 사용하고 있고, 문제가 되는 JNDI와 LDAP를 사용 중이라면 긴급조치를 해야 할 것입니다.

하지만 현재 운영 중인 서버에 JNDI와 LDAP를 사용하고 있지 않다면 어떻게 해야 할까요?

앞으로도 JNDI와 LDAP를 사용하지 않는다면 Log4j 취약점 버전을 그대로 유지해도 될까요?

현재 이슈는 개발자 커뮤니티뿐 아니라 사회적으로 이슈가 되었습니다.

현재 운영 중인 서비스가 있다면 서비스를 이용하는 고객에게 문의가 올 수 있습니다.

답변으로 우리 회사 서비스는 취약점에 포함된 버전을 사용하고 있지만 JNDI와 LDAP를 사용하지 않으니 변경할 예정이 없다고 한다면 고객이 이해할 수 없다고 생각합니다.

고객에게 문의가 왔다면 Log4j 업데이트를 우선순위로 둬서 작업을 진행해야 할 것입니다.

하지만 고객에게 문의가 오지 않는다면 일정을 잡아서 취약점에 해당하는 버전들은 업데이트를 해야 할 것입니다.

감사합니다.

 


 
참고사이트

[4] 취약점 정보 : https://nvd.nist.gov/vuln/detail/CVE-2021-44832
[5] Log4j 2.12.4, 2.3.2 다운로드 : https://archive.apache.org/dist/logging/log4j/

 

'스프링 > LOG' 카테고리의 다른 글

[LogIn]의 기원  (0) 2022.01.03

 

링크

 

https://programmers.co.kr/learn/courses/30/lessons/17680

 

코딩테스트 연습 - [1차] 캐시

3 ["Jeju", "Pangyo", "Seoul", "NewYork", "LA", "Jeju", "Pangyo", "Seoul", "NewYork", "LA"] 50 3 ["Jeju", "Pangyo", "Seoul", "Jeju", "Pangyo", "Seoul", "Jeju", "Pangyo", "Seoul"] 21 2 ["Jeju", "Pangyo", "Seoul", "NewYork", "LA", "SanFrancisco", "Seoul", "Ro

programmers.co.kr

 

문제

 


 

풀이

 

  • 캐시 교체 알고리즘은 LRU(Least Recently Used)를 사용한다.
  • cache hit일 경우 실행시간은 1이다.
  • cache miss일 경우 실행시간은 5이다.

 

1. 캐시 교체 알고리즘은 LRU(Least Recently Used)를 사용한다.

LRU 알고리즘은 사용된 데이터의 경우 제거 후 순위로 변경해준다.

A, B, C, D 라는 문자가 차례대로 들어온 후

캐시에서 제거될 때 A, B, C, D 순서로 제거된다.

E 문자열 삽입

A는 제거되고 그 뒤에 있던 B가 다음 제거 타겟이다. 

 

여기에서 중간에 있는 C를 요청받으면

이미 C가 존재하기 때문에 제거되는 데이터는 없다.

하지만 순서가 바뀐다.

B 다음으로 C가 제거 타겟이였지만 

C가 새로 들어온 신입처럼 후순위로 배치된다.

이것이 LRU 알고리즘이다.

 

2. cache hit일 경우 실행시간은 1이다.

데이터가 들어왔을 때 캐시에 존재하는 데이터면 +1

 

3. cache miss일 경우 실행시간은 5이다.

데이터가 들어왔을 때 캐시에 없는 데이터면 +5


 

캐시 사이즈 : 3

*대소문자를 구분하지 않기 때문에 일괄적으로 대문자로 변환하여 비교하였습니다.

 

데이터 ["Jeju", "Pangyo", "Seoul", "NewYork", "LA"]

JEJU 는 캐시에 없는 데이터 

캐시 [JEJU]

수행 시간(0) : +5

 

데이터 ["Pangyo", "Seoul", "NewYork", "LA"]

PANGYO 는 캐시에 없는 데이터

캐시 [JEJU, PANGYO]

수행 시간(5) : +5

 

데이터 ["Seoul", "NewYork", "LA"]

SEOUL 은 캐시에 없는 데이터

캐시 [JEJU, PANGYO, SEOUL]

수행 시간(10) : +5

 

데이터 ["NewYork", "LA"]

NEWYORK 은 캐시에 없는 데이터

캐시 사이즈에 자리가 없기 때문에 제일 먼저 들어온 데이터 제거 후 삽입

[JEJU, PANGYO, SEOUL] 

[PANGYO, SEOUL] 

[PANGYO, SEOUL, NEWYORK

수행 시간(15) : +5

 

데이터 ["LA"]

LA 는 캐시에 없는 데이터

캐시 사이즈에 자리가 없기 때문에 제일 먼저 들어온 데이터 제거 후 삽입

[PANGYO, SEOUL, NEWYORK] 

[SEOUL, NEWYORK] 

[SEOUL, NEWYORK, LA

수행 시간(20) : +5

 

총 수행 시간 : 25초

 


 

위의 로직 구현

 

import java.util.LinkedList;
import java.util.Queue;

class Solution {
	
	/**
	 * 카카오 캐시
	 * @param cacheSize
	 * @param cities
	 * @return
	*/
	public int solution(int cacheSize, String[] cities) {
		int answer 		= 0;
		int count 		= cities.length;
		Queue<String> qu	= new LinkedList<>();
		
		for (int i = 0; i < count; ++i) {
			String str = cities[i].toLowerCase();
			
			// 캐시 사이즈가 0일 경우 밑의 로직 수행 X -> 수행 시간 +5
			if (0 == cacheSize) {
				answer += 5;
				continue;
			}
            
			// 캐시에 존재하는 데이터면 순서 변경 후 수행 시간 +1
			if (qu.contains(str)) {
				qu.remove(str);
				qu.add(str);
				++answer;
				continue;
			}
			
			// 캐시 사이즈에 자리가 없을 경우 먼저 들어온 데이터 제거 후 삽입 수행 시간 +5
			if (qu.size() >= cacheSize) {
				qu.poll();
				qu.add(str);
				answer += 5;
				continue;
			}
            
			// 그 외 캐시에 삽입 후 수행 시간 +5
			qu.add(str);
			answer += 5;
		}
		return answer;
	}
}

 


 

후기

* 난이도 (5점 만점)

5 : 풀 줄 알면 기업 코딩테스트는 문제 없음.

4 : 평균적인 기업 코딩테스트의 중간 이상.

3 : 평균적인 기업 코딩테스트의 쉬운 문제 .

2 : 알고리즘 문제를 연습하고 있다면 풀 수 있는 문제.

1 : 시간이 오래 걸리지 않고, 누구나 풀 수 있는 문제.

 

난이도는 생각보다 쉬운 편이었으며 LRU 알고리즘을 이해한다면 충분히 풀 수 있다.

LRU 알고리즘을 풀기 위해 자료구조 Queue 를 사용했고 어려운 기술은 없었다.

 

 

링크

https://programmers.co.kr/learn/courses/30/lessons/17682

 

코딩테스트 연습 - [1차] 다트 게임

 

programmers.co.kr


 

문제

 


풀이
  1. 다트 게임은 총 3번의 기회로 구성된다.
  2. 각 기회마다 얻을 수 있는 점수는 0점에서 10점까지이다.
  3. 점수와 함께 Single(S), Double(D), Triple(T) 영역이 존재하고 각 영역 당첨 시 점수에서 1제곱, 2제곱, 3제곱 (점수1 , 점수2 , 점수3 )으로 계산된다.
  4. 옵션으로 스타상(*) , 아차상(#)이 존재하며 스타상(*) 당첨 시 해당 점수와 바로 전에 얻은 점수를 각 2배로 만든다. 아차상(#) 당첨 시 해당 점수는 마이너스된다.
  5. 스타상(*)은 첫 번째 기회에서도 나올 수 있다. 이 경우 첫 번째 스타상(*)의 점수만 2배가 된다. (예제 4번 참고)
  6. 스타상(*)의 효과는 다른 스타상(*)의 효과와 중첩될 수 있다. 이 경우 중첩된 스타상(*) 점수는 4배가 된다. (예제 4번 참고)
  7. 스타상(*)의 효과는 아차상(#)의 효과와 중첩될 수 있다. 이 경우 중첩된 아차상(#)의 점수는 -2배가 된다. (예제 5번 참고)
  8. Single(S), Double(D), Triple(T)은 점수마다 하나씩 존재한다.
  9. 스타상(*), 아차상(#)은 점수마다 둘 중 하나만 존재할 수 있으며, 존재하지 않을 수도 있다.

 

조건을 하나씩 적용하겠습니다.

1. 다트 게임은 총 3번의 기회로 구성된다.

총 3개의 값을 구합니다.

 

2. 각 기회마다 얻을 수 있는 점수는 0점에서 10점까지이다.

입력 받는 점수 범위는 0~10 입니다.

 

3. 점수와 함께 Single(S), Double(D), Triple(T) 영역이 존재하고 각 영역 당첨 시 점수에서 1제곱, 2제곱, 3제곱 (점수1 , 점수2 , 점수3 )으로 계산된다.

S : 1제곱 

D : 2제곱

T : 3제곱

 

4. 옵션으로 스타상(*) , 아차상(#)이 존재하며 스타상(*) 당첨 시 해당 점수와 바로 전에 얻은 점수를 각 2배로 만든다. 아차상(#) 당첨 시 해당 점수는 마이너스된다.

* : 현재 점수, 이전 점수 X 2

# : 현재 점수 X (-1)

 

5. 스타상(*)은 첫 번째 기회에서도 나올 수 있다. 이 경우 첫 번째 스타상(*)의 점수만 2배가 된다.

* : 이전 점수가 없다면 현재 점수만 X 2

 

6. 스타상(*)의 효과는 다른 스타상(*)의 효과와 중첩될 수 있다. 이 경우 중첩된 스타상(*) 점수는 4배가 된다.

4, 5번 조건을 적용해주면 6번도 해결됩니다.

 

7. 스타상(*)의 효과는 아차상(#)의 효과와 중첩될 수 있다. 이 경우 중첩된 아차상(#)의 점수는 -2배가 된다.

4, 5번 조건을 적용해주면 7번도 해결됩니다.

 

8. Single(S), Double(D), Triple(T)은 점수마다 하나씩 존재한다.

3번의 다트 점수 중 S, D, T 은 하나씩 존재하므로 중복 체크 없이 구현하면 됩니다.

 

9. 스타상(*), 아차상(#)은 점수마다 둘 중 하나만 존재할 수 있으며, 존재하지 않을 수도 있다.

3번의 다트 점수 중 *, # 조건이 있을때 적용하면 됩니다.


위의 설명대로 하나씩 구현하면 됩니다.

dartResult : 1S2D*3T

받은 dart 값을 나눠보면

1S

2D* (현재 점수와 이전 점수 X 2) -> 위의 1S도 X 2 

3T

이렇게 볼 수 있고

1^1 * 2

2^2 * 2

3^3

이렇게 완성됩니다.

반환은 3개 점수 합 입니다.


 

위의 로직 구현

class Solution {
	
	/**
	 * 카카오 다트 게임
	 * @param dartResult
	 * @return
	*/
	public int solution(String dartResult) {

		int count	= dartResult.length();
		int bonus 	= 0;
 		int[] arr 	= new int[3];
		int arrCnt 	= 0;
		for (int i = 0; i < count; ++i) {
			char ch =  dartResult.charAt(i);
			if (ch == 49) {
 				if (dartResult.charAt(i+1) == 48) {
					ch = 58;
					++i;
				}
			}
    		
			//옵션 로직
			switch(ch) {
			case '*':
				arr[arrCnt-1] *= 2;
				if (arrCnt-1 > 0) arr[arrCnt-2] *= 2;
				continue;
			case '#':
				arr[arrCnt-1] *= (-1);
				continue;
			}
    		
			//제곱 로직
			switch(dartResult.charAt(i+1)) {
			case 'S':
				bonus = 1;
				break;
			case 'D':
				bonus = 2;
				break;
			case 'T':
				bonus = 3;
				break;
			}
    		
			arr[arrCnt] = (int) Math.round(Math.pow(ch-'0', bonus));
			++arrCnt;
			++i;
		}
		return arr[0]+arr[1]+arr[2];
	}  
}


후기

* 난이도 (5점 만점)

5 : 풀 줄 알면 기업 코딩테스트는 문제 없음.

4 : 평균적인 기업 코딩테스트의 중간 이상.

3 : 평균적인 기업 코딩테스트의 쉬운 문제 .

2 : 알고리즘 문제를 연습하고 있다면 풀 수 있는 문제.

1 : 시간이 오래 걸리지 않고, 누구나 풀 수 있는 문제.

 

난이도는 생각보다 쉬운 편이었으며 문제를 이해하고 천천히 풀면 충분히 풀 수 있다.

어려운 기술이 들어간 것은 없고, 말 그대로 문제 내용을 하나씩 구현한 것이다.

 

 

링크

 

https://programmers.co.kr/learn/courses/30/lessons/17681

 

코딩테스트 연습 - [1차] 비밀지도

비밀지도 네오는 평소 프로도가 비상금을 숨겨놓는 장소를 알려줄 비밀지도를 손에 넣었다. 그런데 이 비밀지도는 숫자로 암호화되어 있어 위치를 확인하기 위해서는 암호를 해독해야 한다. 다

programmers.co.kr


 

문제

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

풀이

 

n = 5

arr1 = [9, 20, 28, 18, 11]

arr2 = [30, 1, 21, 17, 28]

배열의 값을 2진수로 변환 후 지도에 표시

2진수는 1과 0으로 구성.

1과 0은 각각 지도 표기 방법으로 바꿔준다.

1 = "#"

0 = " "  (공백)

 

9를 2진수로 변환하면 1001 라는 값이 나온다.

 

20을 2진수로 변환하면 10100

 

위의 9와 20을 2진수로 변환한 값을 보면 자릿수가 다르다.

자릿수를 맞춰주기 위해서는 처음에 받은 n 만큼 자릿수를 채워준다.

그럼 밑의 내용처럼 9를 2진수로 변환한 내용도 바뀐다. 

1001 -> 01001

위의 내용대로 arr1 의 값을 2진수로 모두 변환시키면

arr1 = [9, 20, 28, 18, 11]

이렇게 된다.

이 것을 지도로 변환시키면

이렇게 표기가 된다.

 

 

arr2도 2진수로 변환

arr2 = [30, 1, 21, 17, 28]

이렇게 된다.

이 것을 지도로 변환시키면

이렇게 표기된다.

 

그럼 arr1 지도와 arr2 지도의 # 부분을 합집합으로 보여주면 된다.

비밀지도가 완성되었다.

return 방식은 String 배열에 담아서 해준다.

["#####","# # #", "### #", "# ##", "#####"]

 

 


 

위의 로직 구현

 

class Solution {
    
	/**
	 * 각 배열의 지도 합치기
	 * @param str1
	 * @param str2
	 * @param n
	 * @return
	*/
	public String sumArr(String str1, String str2, int n) {
		StringBuilder sb = new StringBuilder(); 
		for (int i = 0; i < n; ++i) {
			sb.append(((str1.charAt(i) + str2.charAt(i)) == 96)? " ":"#");
		}
		return sb.toString();
	}
	
	/**
	 * 각 배열의 자릿수 채워주기
	 * @param str
	 * @param n
	 * @return
	*/
	public String lpad(String str, int n) {
		int size		= n - str.length();
		StringBuilder sb	= new StringBuilder();
		for (int i = 0; i < size; ++i) {
			sb.append("0");
		}
		return sb.toString()+str;
	}
    
	/**
	 * 카카오 비밀지도
	 * @param n
	 * @param arr1
	 * @param arr2
	 * @return
	*/
	public String[] solution(int n, int[] arr1, int[] arr2) {
		int arrCount	= arr1.length;
		String[] arrStr	= new String[arrCount];
		String[] answer	= new String[arrCount];
        
		for (int i = 0; i < arrCount; ++i) {
			String str		= lpad(Integer.toBinaryString(arr1[i]), n);
			StringBuilder sb	= new StringBuilder();
			for (int j = 0; j < str.length(); ++j) {
				sb.append(str.charAt(j));
			}
			arrStr[i] = sb.toString();
		}
		
		for (int i = 0; i < arrCount; ++i) {
			String str	= lpad(Integer.toBinaryString(arr2[i]), n);
			answer[i]	= sumArr(arrStr[i], str, n);
		}
		return answer;
	}
}


 

후기

* 난이도 (5점 만점)

5 : 풀 줄 알면 기업 코딩테스트는 문제 없음.

4 : 평균적인 기업 코딩테스트의 중간 이상.

3 : 평균적인 기업 코딩테스트의 쉬운 문제 .

2 : 알고리즘 문제를 연습하고 있다면 풀 수 있는 문제.

1 : 시간이 오래 걸리지 않고, 누구나 풀 수 있는 문제.

 

난이도는 생각보다 쉬운 편이었으며 문제를 이해하고 천천히 풀면 충분히 풀 수 있다.

어려운 기술이 들어간 것은 없고, 말 그대로 문제 내용을 하나씩 구현한 것이다.

링크

 

https://www.acmicpc.net/problem/18406

 

18406번: 럭키 스트레이트

첫째 줄에 점수 N이 정수로 주어진다. (10 ≤ N ≤ 99,999,999) 단, 점수 N의 자릿수는 항상 짝수 형태로만 주어진다.

www.acmicpc.net


문제

 


풀이

 

* 입력 받는 자릿수는 짝수 형태로만 주어진다.

짝수라는 말은 전체 수가 짝수가 아니라 자릿수가 짝수 입니다.

 

 

첫 번째 예제

123402를 입력 받았을 때

123402 의 자릿수는 6 이라서 짝수.

입력 받은 자릿수를 반반 나누어 합산.

 

*절반 왼쪽

1 + 2 + 3 = 6

 

*절반 오른쪽

4 + 0 + 2 = 6

 

왼쪽과 오른쪽 합이 둘 다 6이므로 결과는 같다.

결과가 같으면 "LUCKY" 라는 문자열 반환.

 

 

 

두 번째 예제

7755 의 자리수는 4 라서 짝수

입력 받은 자릿수를 반반 나누어 합산.

 

*절반 왼쪽

7 + 7 = 14

 

*절반 오른쪽

5 + 5 = 10

 

왼쪽 합 : 14

오른쪽 합 : 10

 

14 와 10은 같지 않음.

왼쪽 합과 오른쪽 합이 같지 않으면 "READY" 라는 문자열 반환.

 


받은 숫자 자릿수를 절반씩 나누어서 합산을 한 뒤 비교해보는 방식으로 풀었습니다.

 

위의 로직 구현

 

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
 
public class Main {
 
	/**
	 * 럭키 스트레이트
	 * @param args
	 * @throws IOException
	 */
	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		String str 	= br.readLine();
		int count 	= str.length();
		int left	= 0;
		int right	= 0;
		for (int i = 0; i < count/2; ++i) {
			left += (str.charAt(i));
		}
		for (int i = count/2; i < count; ++i) {
			right += (str.charAt(i));
		}
		System.out.println((left == right ? "LUCKY":"READY"));
	}
}

 

후기

* 난이도 (5점 만점)

5 : 풀 줄 알면 기업 코딩테스트는 문제 없음.

4 : 평균적인 기업 코딩테스트의 중간 이상.

3 : 평균적인 기업 코딩테스트의 쉬운 문제 .

2 : 알고리즘 문제를 연습하고 있다면 풀 수 있는 문제.

1 : 시간이 오래 걸리지 않고, 누구나 풀 수 있는 문제.

 

난이도는 매우 쉬운 편이었으며 for문을 사용할 때 길이를 지정해서 사용할 줄 알면 어렵지 않다.

+ Recent posts