반응형

POJO 구성 방식

14. JoinPoint 정보 가져오기

AOP 에서 어드바이스는 여러 조인포인트, 즉 프로그램 실행지점 곳곳에 적용된다. 어드바이스가 정확하게 작동하려면 조인포인트에 대한 세부 정보가 필요한 경우가 존재한다.

 

어드바이스 메소드의 Signature 에 org.aspectj.lang.JoinPoint 형 인수를 선언하면 조인포인트 정보를 얻을 수 있다. 다음과 같이 logJoinPoint 어드바이스에서 조인포인트 정보를 액세스한다고 하자. 필요한 정보는 조인포인트 유형, 메소드 시그니처, 인수값, 대상 객체와 프록시 객체이다.

@Aspect
@Component
public class CalculatorLoggingAspect {
	
    private Logger log = LoggerFactory.getLogger(this.getClass());
    
    @Before("execution(* *.*(..))") 
    public void logJoinPoint(JoinPoint joinPoint) {
        log.info("Join point kind : {}", joinPoint.getKind());
        log.info("Signature declaring type : {}", joinPoint.getSignature().getDeclaringTypeName());
        log.info("Signature name : {}", joinPoint.getSignature().getName());
        log.info("Arguments : {}", Arrays.toString(joinPoint.getArgs()));
        log.info("Target class : {}", joinPoint.getTarget().getClass().getName());
        log.info("This class : {}", joinPoint.getThis().getClass().getName());
    }
}

 

프록시로 감싼 원본 빈은 Target Object 라고 하며 프록시 객체는 this 로 참조한다. 대상 객체와 프록시 객체는 각각 조인포인트에서 getTarget(), getThis() 메소드로 가져올 수 있다. 실행 결과는 다음과 같다.

더보기

Join point kind : method-execution
Signature declaring type : com.apress.springrecipes.calculator.ArithmeticCalculator
Signature name : add
Arguments : [1.0, 2.0]
Target class : com.apress.springrecipes.calculator.ArithmeticCalculatorImpl
This class : com.sun.proxy.$Proxy18

 

15. @Order 로 애스펙트 우선순위 설정

같은 조인포인트에 애스펙트를 여러 개 적용할 경우, 우선순위를 정해야 한다. 애스펙트 간 우선순위는 Ordered 인터페이스를 구현하거나 @Order 어노테이션을 붙여 지정할 수 있다.

 

@Aspect
@Component
public class CalculatorVaildationAspect {
	
    @Before("execution(* *.*(double, double))")
    public void validateBefore(JoinPoint joinPoint) {
    	for (Object arg : joinPoint.getArgs()) {
        	validate((Double) arg);
        }
    }
    
    private void validate(double a) {
    	if (a < 0) {
        	throw new IllegalArgumentException("Positive Numbers only");
        }
    }
}

@Aspect
@Component
public class CalculatorLoggingAspect {
	
    @Before("execution(* *.*(..))")
    public void logBefore(JoinPoint joinPoint) {
    	// ...
    }
}

 

위와 같이 두 애스펙트가 존재할 경우 어느 쪽을 먼저 적용해야 할 지 알 수가 없다. 이런 경우 어느 한 애스팩트가 다른 것보다 먼저 실행되게 하기 위해 우선순위를 설정해야 한다. 두 애스펙트 모두 Ordered 인터페이스를 구현하거나 @Order 어노테이션을 활용하면 된다.

 

먼저 Ordered 인터페이스를 구현할 경우, getOrder() 메소드가 반환하는 값이 작을수록 우선순위가 높다. 다음과 같이 작성할 경우 검증 애스펙트가 로깅 애스펙트보다 우선순위가 더 높아진다.

@Aspect
@Component
public class CalculatorVaildationAspect implements Ordered {
	
    ...
    public int getOrder() {
    	return 0;
    }
}

@Aspect
@Component
public class CalculatorLoggingAspect implements Ordered {
	
    ...
    public int getOrder() {
    	return 1;
    }
}

 

아래와 같이 @Order 에 우선순위값을 넣으면 더 깔끔하게 구현이 가능하다.

@Aspect
@Component
@Order(0)
public class CalculatorVaildationAspect { ... }

@Aspect
@Component
@Order(1)
public class CalculatorLoggingAspect { ... }

 

16. 애스펙트 포인트컷 재사용

포인트컷 표현식을 여러 번 되풀이해 사용할 경우에, 어드바이스 어노테이션에 직접 써넣는 것보다 재사용할 방법을 필요로 하게 된다. @Pointcut 어노테이션을 활용하면 포인트컷만 따로 정의해 여러 어드바이스에서 재사용이 가능하다.

 

애스펙트에서 포인트컷은 @Pointcut 을 붙인 단순 메소드로 선언할 수 있다. 포인트컷과 어플리케이션 로직이 뒤섞이는 것은 바람직하지 않으니 메소드 바디는 보통 비워두고 포인트컷의 가시성은 메소드의 수정자로 조정한다. 이렇게 선언한 포인트컷은 다른 어드바이스가 메소드명으로 참조할 수 있다.

 

@Aspect
@Component
public class CalculatorLogginAspect {
	
    ...
    @Pointcut("execution(* *.*(..))")
    private void loggingOperation() {}
    
    @Before("loggingOperation()")
    public void logBefore() { ... }
    
    @After("loggingOperation()")
    public void logAfter() { ... }
}

 

여러 애스펙트가 포인트컷을 공유하는 경우라면 공통 클래스 한 곳에 포인트컷을 모아두는 편이 좋다. 이 때 포인트컷 메소드는 public 으로 선언한다.

@Aspect
public class CalculatorPointcuts {
	
    @Pointcut("execution(* *.*(..))")
    public void loggingOperation() {}
}

 

외부 클래스에 있는 포인트컷을 참조할 때는 클래스명도 함께 적는다. 만약 다른 패키지에 있는 경우는 패키지명까지 기재한다.

@Aspect
@Component
public class CalculatorLoggingAspect {
	
    @Before("CalculatorPointcuts.loggingOperation()")
    public void logBefore() { ... }
    
    @After("CalculatorPointcuts.loggingOperation()")
    public void logAfter() { ... }
}

 

#Reference.

 

스프링 5 레시피(4판)

이 책은 스프링 5에 새로 탑재된 기능 및 다양한 구성 옵션 등 업데이트된 프레임워크 전반을 실무에 유용한 해법을 제시하는 형식으로 다룹니다. IoC 컨테이너 같은 스프링 기초부터 스프링 AOP/A

www.hanbit.co.kr

 

반응형

스프링 AOP (Aspect-Oriented Programming)

AOP 란, Aspect-Oriented Programming 의 약자로 관점 지향 프로그래밍이라 불린다. 관점 지향은 쉽게 표현하자면 어떤 로직을 핵심적인 관점, 부가적인 관점으로 나누어 보고 그 관점을 기준으로 각각 모듈화하겠다는 것이다. 여기서 모듈화란, 공통한 로직이나 기능을 하나로 묶는 것을 뜻한다.

 

AOP 에서 관점을 기준으로 로직을 모듈화한다는 것은 코드들을 부분적으로 나누어 모듈화하겠다는 뜻이다. 이 때, 소스 코드상 다른 부분에서 반복해서 사용되는 코드들을 발견할 수 있는데 이것을 Crosscutting Concerns (흩어진 관심사) 라고 부른다.

 

Crosscutting Concerns 의 모듈화

위와 같이 흩어진 관심사들을 Aspect 로 모듈화하고 핵심적인 비즈니스 로직에서 분리해 재사용하는 것이 AOP 의 취지이다.

 

OOP 와 AOP 개념 비교

더보기
  • OOP : 비즈니스 로직의 모듈화
    • 모듈화의 핵심 단위는 비즈니스 로직
  • AOP : 인프라 혹은 부가기능의 모듈화
    • 각 모듈들의 주요 목적 외 필요한 부가적인 기능들
    • 대표적 예 : 로깅, 트랜잭션, 보안 등

 

AOP 주요 개념

  • Aspect : 위에서 설명한 흩어진 관심사를 모듈화한 것
  • Target : Aspect 를 적용한 곳 (클래스, 메소드, ...)
  • Advice : 실질적으로 어떤 동작을 해야할 지에 대한 것
  • JoinPoint : 어드바이스가 적용될 위치
  • PointCut : 부가기능이 적용될 대상을 선정하는 방법의 표현
  • Proxy : 타겟을 감싸서 타겟의 요청을 대신 받아주는 래핑 오브젝트
  • Introduction : 타겟 클래스에 코드 변경없이 신규 메소드나 멤버 변수를 추가하는 기능
  • Weaving : 지정된 객체에 Aspect 를 적용해 새로운 프록시 객체를 생성하는 과정
반응형

POJO 구성 방식

11. 스프링 환경 및 프로파일마다 다른 POJO 로드

자바 구성 클래스를 여러 개 생성하고 각 클래스마다 POJO 인스턴스 / 빈을 묶는다. 이렇게 작성한 구성 클래스에 @Profile 을 붙임으로써 프로파일 별로 다른 빈 인스턴스 구성을 할 수 있다.

 

POJO 의 초깃값은 애플리케이션의 제작 단계에 따라 달라질 수 있다. 예를 들어 개발 단계나 운영 단계에서 사용하고자 하는 DB 의 경로가 달라질 수 있고, 이러한 설정들을 달리 적용해야 할 빈들이 존재할 수 있다. 이런 경우에 @Profile 을 사용해 개발자가 시나리오에 맞는 구성 클래스를 읽어 들여 실행하게 할 수 있다.

 

간단한 예시로, 자바 구성 클래스에 @Profile 을 붙여 여러 버전으로 나누는 코드를 작성한다.

@Configuration
@Profile("develop")
public class ConfigurationDevelop {
	
    @Bean
    public Product product() {
    	// ...
    }
}
@Configuration
@Profile("deploy")
public class ConfigurationDeploy {
	
    @Bean
    public Product product() {
    	// ...
    }
}

 

위와 같이 구성 클래스를 @Profile 을 나눠 작성한다. @Profile 의 속성으로 위의 예제에서는 하나의 속성값만을 정의했지만, 두 개 이상의 속성값을 정의하는 것도 가능하고 이 경우는 {} 로 값들을 묶어 정의할 수 있다. 이렇게 작성한 프로파일들을 활성화하기 위해서 어플리케이션 콘텍스트 환경을 가져온 뒤 다음과 같이 setActiveProfiles() 메소드를 호출한다.

AnnotationConfigApplicationContext context = ...;
context.getEnvironment().setActiveProfiles("develop");

혹은 자바 런타임 플래그로 프로파일을 명시하는 것도 가능한데, 'develop' 프로파일을 활성화하려면 다음 플래그를 추가하는 것도 가능하다.

-Dspring.profiles.active=develop

 

어떤 프로파일도 로드되지 않는 불상사를 막기 위해 기본 프로파일을 지정하는 것이 가능하다. 어플리케이션 콘텍스트 환경의 메소드를 통해 이를 지정하려면 setDefaultProfiles() 메소드를 사용할 수 있고, 플래그로 지정하고자 할 경우는 spring.profiles.active 대신 spring.profiles.default 로 사용할 수 있다.

 

12. POJO 에게 IoC 컨테이너 리소스 알려주기

빈이 IoC 컨테이너 리소스를 인지하게 하려면 Aware 인터페이스를 구현한다. 스프링은 이 인터페이스를 구현한 빈을 감지해 대상 리소스를 세터 메소스로 주입한다. 자주 쓰이는 스프링 Aware 인터페이스는 다음과 같다.

Aware 인터페이스 대상 리소스 타입
BeanNameAware IoC 컨테이너에 구성한 인스턴스의 빈 이름
BeanFactoryAware 컨테이너 서비스를 호출하는 데 사용
ApplicationContextAware 컨테이너 서비스를 호출하는 데 사용
MessageSourceAware 텍스트 메시지 해석하는 데 사용
ApplicationEvent
PublisherAware
애플리케이션 이벤트를 발행하는 데 사용
ResourceLoaderAware 외부 리소스를 로드하는 데 사용
EnvironmentAware ApplicationContext 인스턴스에 묶인 org.springframework.core.env.Environment 인스턴스

Aware 인터페이스의 세터 메소드는 스프링이 빈 프로퍼티를 설정한 후, 초기화 콜백 메소드를 호출하기 이전에 호출한다. 순서를 정리하면 다음과 같다.

  1. 생성자나 팩토리 메소드를 호출해 빈 인스턴스 생성
  2. 빈 프로퍼티에 값, 빈 레퍼런스 설정
  3. Aware 인터페이스에 정의한 세터 메소드 호출
  4. 빈 인스턴스를 빈 후처리기의 postProcessBeforeInitialization() 메소드로 넘겨 초기화 콜백 메소드 호출
  5. 빈 인스턴스를 빈 후처리기의 postProcessAfterInitialization() 메소드로 넘김
  6. 컨테이너가 종료되면 폐기 콜백 메소드 호출
NOTE
스프링 최신 버전에서는 @Autowired 를 붙여 ApplicationContext 를 손쉽게 가져올 수 있어 굳이 Aware 인터페이스를 구현할 필요가 없다. 프레임워크나 라이브러리 개발 시에는 Aware 인터페이스를 구현하는 게 나을 수 있지만, 스프링에 종속된 인터페이스를 정말 구현할 필요가 있는지 따져보는 것이 중요하다.

 

13. 어노테이션을 활용한 AOP 적용

AOP (Aspect-Oriented Programming ; 관점 지향 프로그래밍).

AOP 에 대한 개념은 따로 포스팅하도록 하겠다. 우선 스프링에서 어노테이션을 활용해 어떻게 AOP 를 적용하는 지를 먼저 정리하려 한다.

 

자바 클래스에 @Aspect 를 붙이고 메소드 별로 적절한 어노테이션을 붙여 Advice 로 만들어 Aspect 를 정의할 수 있다. 적용 가능한 Advice 어노테이션은 @Before, @After, @AfterReturning, @AfterThrowing, @Around 가 있다. IoC 컨테이너에서 Aspect 어노테이션 기능을 활성화하기 위해서는 구성 클래스 중 하나에 @EnableAspectJAutoProxy 를 붙인다.

 

Aspect 는 여러 타입과 객체에 공통 관심사(예 ; 로깅, 트랜잭션 관리) 를 모듈화한 자바 클래스로, @Aspect 를 붙여 표시한다. AOP 에서 말하는 Aspect 란 어디에서 (PointCut), 무엇을 할 것인지 (Advice) 를 합쳐 놓은 개념이다. 어드바이스는 @Advice 를 붙인 단순 자바 메소드이고, 포인트컷은 어드바이스에 적용할 타입 / 객체를 찾는 표현식이다.

 

@Before 어드바이스

Before 어드바이스는 특정 프로그램 실행 시점 이전의 공통 관심사를 처리하는 메소드이다.

@Aspect
@Component
public class CalculatorLoggingAspect {
	
    private Logger log = LoggerFactory.getLogger(this.getClass());
    
    @Before("execution(* Calculator.add(..))")
    public void logBefore() {
        log.info("The method add() begins");
    }
}

위와 같이 @Before 어노테이션의 값으로 포인트컷 표현식을 적용할 수 있다. 이 포인트컷 표현식은 Calculator 인터페이스의 add() 메소드 실행을 가리킨 것이다. 앞부분의 와일드카드(*) 는 모든 수정자(public, protected, private), 모든 반환형을 매치함을 의미한다. 인수 목록 부분에 쓴 두 점(..) 은 인수 개수는 몇 개든 좋다는 의미이다.

 

NOTE
@Aspect 만 붙여서는 스프링이 자동으로 감지하지 않기 때문에, 해당 POJO 마다 개별적으로 @Component 를 붙여야 한다.

 

포인트컷으로 매치한 실행 지점을 JoinPoint 라고 한다. 포인트컷은 여러 조인포인트를 매치하기 위해 지정한 표현식이고 이렇게 매치된 조인포인트에서 해야 할 일이 바로 어드바이스이다.

 

어드바이스가 현재 조인포인트의 세부 내용에 접근하려면 JoinPoint 형 인수를 어드바이스 메소드에 선언해야 한다. 그러면 메소드명, 인수값 등 자세한 조인포인트 정보를 조회할 수 있다. 다음과 같이 클래스명, 메소드명에 와일드카드를 적용하면 모든 메소드에 예외없이 포인트컷을 적용할 수 있다.

@Aspect
@Component
public class CalculatorLoggingAspect {
	
    private Logger log = LoggerFactory.getLogger(this.getClass());
    
    @Before("execution(* *.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        log.info("The method {}() begins with {} ", 
            joinPoint.getSignature().getName(), 
            Arrays.toString(joinPoint.getArgs()));
    }
}

 

@After 어드바이스

After 어드바이스는 조인포인트가 끝나면 실행되는 메소드이다. 조인포인트가 정상 실행되든, 도중 예외가 발생하든 상관없이 실행된다.

@Aspect
@Component
public class CalculatorLoggingAspect {
	
    private Logger log = LoggerFactory.getLogger(this.getClass());
    
    @After("execution(* *.*(..))")
    public void logAfter(JoinPoint joinPoint) {
        log.info("The method {}() ends", joinPoint.getSignature().getName());
    }
}

 

@AfterReturning 어드바이스

After 어드바이스는 조인포인트의 실행 성공 여부와 관계없이 동작한다. 조인포인트가 값을 반환할 경우에만 메소드를 실행하고자 한다면, 다음과 같이 AfterReturning 어드바이스로 대체할 수 있다.

@Aspect
@Component
public class CalculatorLoggingAspect {
	
    private Logger log = LoggerFactory.getLogger(this.getClass());
    
    @AfterReturning(
    	pointcut = "execution(* *.*(..))",
    	returning = "result")
    public void logAfterReturning(JoinPoint joinPoint, Object result) {
        log.info("The method {}() ends with {}", joinPoint.getSignature().getName(), result);
    }
}

조인포인트가 반환한 결과값을 받아오려면 @AfterReturning 의 returning 속성으로 지정한 변수명을 어드바이스 메소드의 인자로 지정한다. 스프링 AOP 는 런타임에 조인포인트의 반환값을 이 인수에 넣어 전달한다. 이 때 포인트컷 표현식은 pointcut 속성으로 따로 지정해야 한다.

 

@AfterThrowing 어드바이스

After Throwing 어드바이스는 조인포인트 실행중 예외가 발생할 경우에만 실행된다. 작동 원리는 @AfterReturning 과 같고, 발생한 예외는 @AfterThrowing 의 throwing 속성에 담아 전달할 수 있다. 자바 언어에서 Throwable 은 모든 에러/예외 클래스의 상위 타입이므로 다음과 같이 어드바이스를 적용하면 조인포인트에서 발생한 에러/예외를 모두 가져올 수 있다.

@Aspect
@Component
public class CalculatorLoggingAspect {
	
    private Logger log = LoggerFactory.getLogger(this.getClass());
    
    @AfterThrwoing(
    	pointcut = "execution(* *.*(..))",
    	throwing = "result")
    public void logAfterThrowing(JoinPoint joinPoint, Throwable e) {
        log.error("An exception {} has been thrown in {}()", e, joinPoint.getSignature().getName());
    }
}

 

@Around 어드바이스

Around 어드바이스는 가장 강력한 어드바이스이다. 이 어드바이스는 조인포인트를 완전히 장악하기 때문에 앞서 살펴본 어드바이스를 모두 Around 어드바이스로 조합할 수 있다. 심지어 원본 조인포인트를 언제 실행할 지, 실행 자체를 할지 말지, 계속 실행할 지 여부도 제어할 수 있다.

 

다음은 Before, After Returning, After Throwing 어드바이스를 Around 어드바이스로 조합한 예이다. Around 어드바이스의 조인포인트 인자형은 ProceddingJoinPoint 로 고정돼 있고, JoinPoint 하위 인터페이스인 ProceedingJoinPoint 를 이용하면 원본 조인포인트를 언제 진행할 지 그 시점을 제어할 수 있다.

@Aspect
@Component
public class CalculatorLoggingAspect {
	
    private Logger log = LoggerFactory.getLogger(this.getClass());
    
    @Around("execution(* *.*(..))")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
    	log.info("The method {}() begins with {}",
            joinPoint.getSignature().getName(),
            Arrays.toString(joinPoint.getArgs()));
            
        try {
            Object result = joinPoint.proceed();
            log.info("The method {}() ends with {}", joinPoint.getSignature.getName(), result);
            return result;
        } catch (IllegalArgumentException e) {
            log.error("Illegal Argument {} in {}()", Arrays.toString(joinPoint.getArgs()),
                joinPoint.getSignature().getName());
            throw e;
        }
    }
}

 

#Reference.

 

스프링 5 레시피(4판)

이 책은 스프링 5에 새로 탑재된 기능 및 다양한 구성 옵션 등 업데이트된 프레임워크 전반을 실무에 유용한 해법을 제시하는 형식으로 다룹니다. IoC 컨테이너 같은 스프링 기초부터 스프링 AOP/A

www.hanbit.co.kr

 

반응형

주어진 문제는 위와 같다. 최대 100 개의 도시가 주어지고, 도시 간 이동이 가능한 버스 노선들이 주어졌을 때 모든 도시 간 이동 시의 최소 비용을 구하는 것이다. 최단 경로 탐색을 위한 알고리즘은 다익스트라, 벨만 포드, A* 탐색 등 여러 가지가 있지만 위와 같이 모든 노드 간 최단 경로를 구하기 위한 알고리즘으로 문제의 이름과 같은 플로이드 와샬 알고리즘이 있다.

 

플로이드 와샬 알고리즘에 대해 내가 기억하고 있는 것은, 노드 간 경로 가중치를 테이블에 저장하고 경로가 존재하지 않는 경우는 INF 값으로 저장한다. 그 뒤 스텝 별로 중간에 다른 노드를 경유하며 경로 가중치를 업데이트하는 방식으로 기억하고 있었다. 하지만 내가 기억하고 있는 알고리즘의 로직은 너무 모호해 문제 풀이에 적용할 수 없었고 따로 플로이드 와샬 알고리즘을 찾아본 뒤 문제를 해결했다.

 

플로이드 와샬 알고리즘

플로이드 와샬 알고리즘을 간단하게 설명하면, O(|V|^3) 의 시간 복잡도를 갖는 모든 쌍 최단 경로 탐색 알고리즘이다. 매 스텝 별로 X to Y (X, Y 는 노드) 경로의 가중치를 업데이트 해야 하므로 O(|V| ^ 2) 를 매 스텝 별로 실행하고, 각 스텝은 경유할 노드를 순차적으로 적용하므로 O(|V| ^ 2 * |V|) 의 시간 복잡도를 갖게 된다.

 

위의 그림을 통해, 알고리즘의 동작 방식을 설명할 수 있다. i, j 는 각각 노드 번호를 뜻하고 k 는 경유할 노드의 번호를 의미한다.

k = 0 일 때, 해당 테이블은 노드를 경유하지 않고 i - j 간 경로를 표현한 것이다.

k = 1 일 때, 변경된 table[2][3] 값은, 기존의 node(2) -> node(3) 의 경로 가중치는 3이고, 노드 1번을 경유한 node(2) -> node(1) -> node(3) 의 가중치 합이 4 + (-2) 이므로 새로운 값이 업데이트된 것을 볼 수 있다.

k = 2 일 때, node(4) -> node(1) 의 경로와 node(4) -> node(3) 의 경로가 2번 노드를 경유함으로 인해 가중치 값이 업데이트된 것을 확인할 수 있다.


이와 같이 경유할 노드를 변경해 가며 경로 가중치 값을 업데이트해 나가면 최종적으로 모든 노드 쌍 간의 최단 경로를 구할 수 있게 된다.

 

알고리즘을 적용한 코드는 3개의 for 문을 중첩함으로써 간단하게 구현이 가능하고, 이를 적용해 문제 해결에 사용한 코드는 다음과 같다.

 

#include <iostream>
using namespace std;

const int CITY = 100;
const int INF = 123456789;

int city, bus;
int dist[CITY + 1][CITY + 1];

void initialize() {
    for (int i = 1; i <= city; i++) {
        for (int j = 1; j <= city; j++) {
            dist[i][j] = INF;
        }
    }
}

void input() {
    cin >> city >> bus;
    initialize();

    for (int i = 0; i < bus; i++) {
        int a, b, c;
        cin >> a >> b >> c;
        dist[a][b] = dist[a][b] < c ? dist[a][b] : c;
    }
}

void output() {
    for (int i = 1; i <= city; i++) {
        for (int j = 1; j <= city; j++) {
            if (dist[i][j] == INF) {
                cout << "0 ";
            } else {
                cout << dist[i][j] << " ";
            }
        }
        cout << "\n";
    }
}

void solve() {
    for (int k = 1; k <= city; k++) {
        for (int i = 1; i <= city; i++) {
            for (int j = 1; j <= city; j++) {
                if (i == j)
                    continue;
                int k_dist = dist[i][k] + dist[k][j];
                dist[i][j] = dist[i][j] < k_dist ? dist[i][j] : k_dist;
            }
        }
    }
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    input();

    solve();

    output();
}

 

# Reference.

 

플로이드-워셜 알고리즘 - 위키백과, 우리 모두의 백과사전

컴퓨터 과학에서, 플로이드-워셜 알고리즘(Floyd-Warshall Algorithm)은 변의 가중치가 음이거나 양인 (음수 사이클은 없는) 가중 그래프에서 최단 경로들을 찾는 알고리즘이다.[1][2] 알고리즘을 한 번

ko.wikipedia.org

'Algorithm > Baekjoon' 카테고리의 다른 글

[BOJ # 1874] 스택 수열  (0) 2021.09.21
[BOJ # 1600] 말이 되고픈 원숭이  (0) 2021.09.19
<BOJ 2508> 나무 자르기  (0) 2019.02.14
반응형

POJO 구성 방식

8. 어노테이션을 이용한 POJO 초기화 / 폐기

어떤 POJO 는 사용하기 전 특정한 초기화 작업, 사용한 후 폐기 작업이 필요하다. 예를 들어 파일을 열거나, 네트워크 / DB 에 접속하거나, 메모리를 할당하는 등 선행 작업이 필요한 경우이다. IoC 컨테이너에서 빈을 초기화하거나 폐기하는 로직을 커스터마이징할 수 있다.

 

자바 구성 클래스의 @Bean 정의 시 initMethod, destroyMethod 속성을 설정함으로써 스프링이 각각을 초기화 / 폐기 콜백 메소드로 인지한다. 혹은 POJO 클래스의 메소드에 @PostConstruct / @PreDestroy 를 붙이는 방법이 있다. 또 @Lazy 를 붙임으로써 Lazy Initialization (주어진 시점까지 빈 생성을 미뤄둠) 을 할 수도 있고, @DependsOn 으로 빈을 생성하기 전에 다른 빈을 먼저 생성하도록 강제할 수 있다.

 

다음과 같이 작업을 수행하고 난 기록을 파일로 기록하는 POJO 클래스를 정의한다.

public class Cashier {
	
    private String fileName;
    private String path;
    private BufferedWriter writer;
    
    // Setter
    
    public void openFile() throws IOException {
    	File targetDir = new File(path);
        if (!targetDir.exists()) {
        	targetDir.mkdir();
        }
        
        File checkoutFile = new File(path, fileName + ".txt");
        if (!checkoutFile.exists()) {
        	checkoutFile.createNewFile();
        }
        
        writer = new BufferedWirter(new OutputStreamWriter(
        	new FileOutputStream(checkoutFile, true)));
    }
    
    // Writer Method
    
    public void closeFile() throws IOException {
    	writer.close();
    }
}

openFile() 메소드는 데이터를 저장할 대상 디렉토리와 파일이 존재하는 지 확인한 뒤, 주어진 시스템 경로에 있는 텍스트 파일을 열어 writer 필드에 할당한다. 기록을 모두 마친 뒤 closeFile() 메소드를 통해 파일을 닫고 시스템 리소스를 반납한다. (기록을 하기 위한 메소드는 생략되었다.)

 

Cashier 빈 생성 이전에 openFile() 메소드를, 폐기 직전에 closeFile() 메소드를 각각 실행하도록 자바 구성 클래스에 빈 정의부를 다음과 같이 설정한다.

@Configuration
public class ShopConfiguration {
	
    @Bean(initMethod = "openFile", destroyMethod = "closeFile")
    public Cashier cashier() {
    	Cashier c1 = new Cashier();
        
        // Call Setter ...
        
        return c1;
    }
}

이로써 Cashier 인스턴스를 생성하기 전에 openFile() 메소드를 먼저 트리거한다. 그리고 빈을 폐기할 때 closeFile() 메소드를 자동 실행하게 된다. 이러한 동작은 다음과 같이 POJO 클래스를 작성해도 똑같이 동작한다.

@Component
public class Cashier {
	
    // Fields and Methods ...
    
    @PostConstruct
    public void openFile() throws IOException {
    	// ... Body
    }
    
    @PreDestroy
    public void closeFile() {
    	// ... Body
    }
}

 

기본적으로 스프링은 모든 POJO 를 미리 초기화한다. 하지만 환경에 따라 빈을 처음으로 요청하기 전까지 초기화 과정을 미루는 게 나을 때도 있다. 이렇듯 나중에 초기화하는 방식을 Lazy Initialization 이라 한다.

 

느긋한 초기화는 시동 시점에 리소스를 집중 소모하지 않아도 되므로 전체 시스템 리소스를 절약할 수 있다. 특히 무거운 작업을 처리하는 POJO 는 느긋한 초기화가 더 어울리는 경우가 있다. 사용 방법은 다음과 같이 POJO 클래스에 @Lazy 를 붙임으로써 느긋한 초기화를 적용할 수 있다. @Lazy 덕분에 다음 클래스는 애플리케이션이 요구하거나 다른 POJO 가 참조하기 전까지는 초기화되지 않는다.

@Component
@Scope("prototype"
@Lazy
public class Product {
	
    // ... Class Body
}

 

POJO 가 늘어나면 그에 따라 초기화 횟수도 증가하고, 여러 자바 구성 클래스에 분산 선언된 많은 POJO 가 서로를 참조하게 되면 Race Condition 이 발생하기 쉽다. B, F 라는 빈의 로직이 C 라는 빈에서 필요하다고 할 때, 아직 스프링이 B, F 빈을 초기화하지 않았는데 C 빈이 먼저 초기화되면 원인불명의 에러가 발생하게 된다.

 

@DependsOn 어노테이션은 어떤 POJO 가 다른 POJO 보다 먼저 초기화되도록 강제하며 설사 그 과정에서 에러가 발생한다 하더라도 헤아리기 쉬운 메시지를 보여준다. 또 @DependsOn 은 빈을 초기화하는 순서를 보장한다.

 

@Configuration 
public class SequenceConfiguration {
	
    @Bean
    @DependsOn("prefixGenerator")
    public SequenceGenerator sequenceGenerator() {
    	SequenceGenerator generator = new SequenceGenerator();
        return generator;
    }
}

 

@DependsOn("prefixGenerator") 를 적용했기 때문에 PrefixGenerator 빈은 항상 SequenceGenerator 보다 먼저 생성된다. @DependsOn 의 속성값은 {} 로 감싼 리스트 형태로 여러 개의 의존성을 설정할 수도 있다.

 

9. PostProcessor 를 통한 POJO 검증 / 수정

빈 후처리기를 이용하면 초기화 콜백 메소드 전후에 원하는 로직을 빈에 적용할 수 있다. 빈 후처리기의 주요한 특징은 IoC 컨테이너 내부의 모든 빈 인스턴스를 대상으로 한다는 점이다. 보통 빈 후처리기는 빈 프로퍼티가 올바른지 체크하거나 어떤 기준에 따라 빈 프로퍼티를 변경 또는 전체 빈 인스턴스를 상대로 어떤 작업을 수행하는 용도로 사용된다.

 

@Required 는 스프링에 내장된 후처리기 RequiredAnnotationBeanPostProcessor 가 지원하는 어노테이션이다. 이 후처리기는 @Required 를 붙인 모든 빈 프로퍼티가 설정되었는지 확인한다.

 

빈 후처리기는 BeanPostProcessor 인터페이스를 구현한 객체이다. 이 인터페이스를 구현한 빈을 발견하면 스프링은 자신이 관장하는 모든 빈 인스턴스에 아래의 두 메소드를 적용한다. 따라서 빈 상태를 조사, 수정, 확인하는 등의 어떠한 로직도 이 메소드에 넣을 수 있다. 이 메소드들은 하는 동작이 없어도 반드시 원본 빈 인스턴스를 반환해야 한다.

@Component
public class CustomBeanPostProcessor implements BeanPostProcessor {
	
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) 
    	throws BeansException {
    
    	return bean;
    }
    
    @Override
    publi Object postProcessAfterInitialization(Object bean, String beanName) 
    	throws BeansException {
    
    	return bean;
    }
}

 

IoC 컨테이너는 자신이 생성한 빈 인스턴스를 모두 하나씩 빈 후처리기에 넘긴다. 만일 특정 타입의 빈만 후처리기를 적용하려면 인스턴스 타입을 체크하는 필터를 이용해 원하는 빈에만 후처리 로직을 적용할 수 있다. 예를 들어 Product 형 빈 인스턴스에만 빈 후처리기를 적용하는 예제는 다음과 같이 작성할 수 있다.

@Component
public class CustomBeanPostProcessor implements BeanPostProcessor {
	
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) 
    	throws BeansException {
    	if (bean instanceof Product) {
        	// ... Some Code
        }
    	return bean;
    }
    
    @Override
    publi Object postProcessAfterInitialization(Object bean, String beanName) 
    	throws BeansException {
        if (bean instanceof Product) {
        	// ... Some Code
        }
    	return bean;
    }
}

위의 두 메소드는 처리한 빈 인스턴스를 반드시 반환해야 한다. 이를 바꿔 말하면, 원본 빈 인스턴스를 다른 인스턴스로 바꿔치기할 수도 있다는 뜻이다.

 

10. 정적 메소드, 인스턴스 메소드, 스프링 FactoryBean 으로 POJO 생성

자바 구성 클래스의 @Bean 메소드는 정적 팩토리를 호출하거나 인스턴스 팩토리 메소드를 호출해 POJO 를 생성할 수 있다. 다음 ProductCreator 클래스에서 정적 팩토리 메소드 createProduct 는 productId 에 해당하는 상품 객체를 생성한다. 주어진 productId 에 따라 인스턴스화할 실제 상품 클래스를 내부 로직으로 결정한다. 맞는 케이스가 없으면 IllegalArgumentException 예외를 던진다.

public class ProductCreator {
	
    public static Product createProduct(String productId) {
    	if (productId.equals("aaa")) {
        	return new Product("AAA", 2.5);
        } else if (productId.equals("cdrw")) {
        	return new Product("CD-RW", 1.5);
        }
        throw new IllegalArgumentException("Unknown Product ID");
    }
}

 

자바 구성 클래스의 @Bean 메소드에서는 일반 자바 구문으로 정적 팩토리 메소드를 호출해 POJO 를 생성한다.

@Configuration
public class ShopConfiguration {
	
    @Bean
    public Product aaa() {
    	return ProductCreator.createProduct("aaa");
    }
    
    @Bean
    public Product cdrw() {
    	return ProductCreator.createProduct("cdrw");
    }
}

 

다음과 같이 클래스 내에 맵을 구성해 상품 정보를 담아두는 방법도 있다. 인스턴스 팩토리 메소드 createProduct() 는 productId 에 해당하는 상품을 맵에서 찾아 반환한다. 마찬가지로 맞는 케이스가 없으면 예외를 던진다.

public class ProductCreator {
	
    private Map<String, Product> products;
    
    public void setProducts(Map<String, Product> products) {
    	this.products = products;
    }
    
    public Product createProduct(String productId) {
    	Product product = products.get(productId);
        if (product != null) {
        	return product;
        }
        throw new IllegalArgumentException("Unknown Product ID");
    }
}

 

ProductCreator 에서 상품을 생성하려면 먼저 @Bean 을 선언해 팩토리값을 인스턴스화하고 이 팩토리의 퍼사드 역할을 하는 두 번째 빈을 선언한다. 마지막으로 팩토리를 호출하고 createProduct() 메소드를 호출해 다른 빈들을 인스턴스화한다.

@Configuration
public class ShopConfiguration {
	
    @Bean
    public ProductCreator productCreatorFactory() {
    	ProductCreator factory = new ProductCreator();
        Map<String, Product> products = new HashMap<>();
        products.put("aaa", new Product("AAA", 2.5));
        products.put("cdrw", new Product("CD-RW", 1.5));
        factory.setProducts(products);
        return factory;
    }
    
    // ... Bean Methods
}

 

다음과 같이 팩토리 빈을 직접 작성하는 방법도 가능하다. 할인율과 상품이 주어지면 할인가가 적용된 상품을 생성하는 팩토리 빈을 작성해 보자. 이 빈은 product, discount 두 프로퍼티를 받아 주어진 상품에 할인가를 계산해 적용하고 상품 빈을 새로 만들어 반환한다.

public class DiscountFactoryBean extends AbstractFactoryBean<Product> {
	
    private Product product;
    private double discount;
    
    public void setProduct(Product product) {
    	this.product = product;
    }
    
    public void setDiscount(double discount) {
    	this.discount = discount;
    }
    
    @Override
    public Class<?> getObjectType() {
    	return product.getClass();
    }
    
    @Override
    protected Product createInstance() throws Exception {
    	product.setPrice(product.getPrice() * (1 - discount));
        return product;
    }
}

팩토리 빈은 제네릭 클래스인 AbstractFactoryBean<T> 를 상속하고, createInstance() 메소드를 오버라이드해 대상 빈 인스턴스를 생성한다. 또 자동 연결 기능이 작동하도록 getObjectType() 메소드로 대상 빈 타입을 반환한다.

 

이제 상품 인스턴스를 생성하는 팩토리 빈에 @Bean 을 붙여 DiscountFactoryBean 을 적용한다.

@Configuration
@ComponentScan("com.apress.springrecipes.shop")
public class ShopConfiguration {
	
    @Bean
    public Product aaa() {
    	Product aaa = new Product("AAA", 2.5);
        return aaa;
    }
    
    @Bean
    public DiscountFactoryBean discountFactoryBeanAAA() {
    	DiscountFactoryBean factory = new DiscountFactoryBean();
        factory.setProduct(aaa());
        factory.setDiscount(0.2);
        return factory;
    }
}

 

 

#Reference.

 

스프링 5 레시피(4판)

이 책은 스프링 5에 새로 탑재된 기능 및 다양한 구성 옵션 등 업데이트된 프레임워크 전반을 실무에 유용한 해법을 제시하는 형식으로 다룹니다. IoC 컨테이너 같은 스프링 기초부터 스프링 AOP/A

www.hanbit.co.kr

 

반응형

주어진 문제는 위와 같다. 수열의 크기 n 이 주어지고, 수열을 이루는 1 ~ n 까지 정수가 하나씩 순서대로 주어진다. 이 때, 스택을 가지고 주어진 수열을 만들기 위해 필요한 연산을 한 줄에 하나씩 출력한다.

 

스택은 LIFO 구조로, push / pop 연산을 통해 자료를 저장하고 뺄 수 있다. 스택으로 수열을 만들기 위해서 필요한 조건은, 주어진 정수가 항상 스택에 저장된 top 값보다 크거나 같아야 한다는 것이다. 만약 스택의 top 에 저장된 값보다 작은 정수가 주어졌을 때는 그 값을 찾기 위해 스택에서 여러 번 pop 을 해야 하므로, 이 문제에서 주어진 조건을 만족할 수 없다.

 

때문에 문제의 풀이 방식은 간단하다. 주어진 정수가 스택에 저장된 top 의 값보다 크다면 값들을 push 해 주어진 정수가 스택에 저장되도록 한다. 그 뒤 스택의 top 에 있는 값을 꺼내면 된다. 만약 주어진 정수가 스택에 저장된 top 값과 같다면 그 값을 pop 연산을 통해 바로 꺼낼 수 있다. 이 때 문제는, 이미 스택에 push 된 값이 다시 push 될 수 있다는 점이다.

 

예를 들어, [1, 2, 3, 4] 의 순서로 push 가 이뤄지고 [4, 3] 순서로 pop 이 되었을 때 스택의 top 에 저장된 값은 '2' 이다. 이 때 '6' 이라는 정수가 주어지면 스택의 top 보다 큰 '3' 부터 '6' 까지 [3, 4, 5, 6] 순서로 push 를 하게 된다. 하지만 이 경우에 이미 [3, 4] 라는 값들은 push / pop 이 된 값이므로 이를 다시 수행하지 않도록 따로 기록을 해야 한다.

 

그래서 이 문제에서 추가적으로 활용하는 변수로 스택에 마지막으로 push 된 값을 기록한다. 만약 위의 경우에 마지막으로 push 한 값이 '4' 임을 안다면 '6' 이 주어졌을 때, [5, 6] 만 push 를 하면 됨을 알 수 있다. 따라서 중복되는 push / pop 연산이 이루어지지 않는다.

 

이러한 풀이로 작성한 소스 코드는 다음과 같다.

 

#include <iostream>
#include <stack>
#include <string>
using namespace std;

int N, num, cur;
stack<int> s;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    string ans = "";
    cin >> N;

    for (int i = 0; i < N; i++) {
        cin >> num;

        if (cur < num) {
            for (int k = cur + 1; k <= num; k++) {
                s.push(k);
                ans += "+\n";
            }
            cur = num;
        }
        else if (s.top() != num) {
            ans = "NO\n";
            break;
        }

        s.pop();
        ans += "-\n";
    }

    cout << ans;
}

문제 풀이 중간에 불가능한 값이 입력될 경우 ans 라는 문자열을 "NO" 로 바꾸고 반복을 종료한다. 그렇지 않을 경우 정상적으로 연산의 순서가 ans 에 저장될 것이다. 이를 콘솔에 출력함으로써 주어진 문제에 대한 답을 얻을 수 있다.

'Algorithm > Baekjoon' 카테고리의 다른 글

[BOJ # 11404] 플로이드  (0) 2021.09.23
[BOJ # 1600] 말이 되고픈 원숭이  (0) 2021.09.19
<BOJ 2508> 나무 자르기  (0) 2019.02.14
반응형

주어진 문제는 위와 같다. 두 개의 문자열 's1', 's2' 가 주어졌을 때, s1 을 scramble 해서 s2 으로 만들 수 있는 지를 확인하는 문제이다. 문자열을 scramble 한다는 것은 위에서 다음과 같이 표현한다.

  1. 문자열의 길이가 1이라면 멈춘다.
  2. 문자열의 길이가 1보다 큰다면, 다음을 따른다.
    • 문자열의 임의 인덱스를 정해서 해당 인덱스를 기준으로 두 개의 문자열로 나눈다. (두 개의 문자열은 빈 문자열이 될 수 없다.)
    • 나눠진 두 개의 문자열을 임의 순서로 배치한다.
    • 나눠진 문자열에 대해 1. 부터 다시 수행한다.

이 문제를 해결하기 위해, 재귀함수를 사용할 수 있었다. 두 문자열을 임의의 인덱스 (모든 인덱스에 대해) 기준으로 각각 두 개의 문자열로 쪼갠다. 그리고 쪼개진 문자열의 순서를 바꿔가며 비교한다.

 

예를 들어, s1 이 "ab", s2 가 "ba" 라고 주어졌다면 각각의 문자열을 두 개로 나눈다. (문자열의 순서를 바꾸며 비교하는 것을 표현하기 위해 극단적으로 길이가 2인 문자열로 예시를 만들었다.) s1 을 두 개의 문자열로 나누면 "a", "b" 가 되고, s2 를 두 개의 문자열로 나누면 "b", "a" 가 된다. 순서를 유지한 채로 비교를 하면 "a" == "b" / "b" == "a" 가 되고, 순서를 바꾸면 "b" == "b" / "a" == "a" 가 된다. 순서를 바꿔 비교했을 때 두 문자열이 같음을 확인할 수 있으므로 s1 과 s2 는 문제의 조건을 만족한다.

 

이와 같이, 각각의 문자열이 주어졌을 때 문자열을 나눠서 다시 그것을 판정하는 방식으로 재귀함수를 작성하면 문제를 해결할 수 있을 것으로 생각했다. 이를 적용한 소스 코드는 다음과 같다.

 

class Solution {
public:
    bool isScramble(string s1, string s2) {
        if(s1==s2)
            return true;
            
        int len = s1.length();
        int count[26] = {0};
        for(int i=0; i<len; i++)
        {
            count[s1[i]-'a']++;
            count[s2[i]-'a']--;
        }
        
        for(int i=0; i<26; i++)
        {
            if(count[i]!=0)
                return false;
        }
        
        for(int i=1; i<=len-1; i++)
        {
            if( isScramble(s1.substr(0,i), s2.substr(0,i)) && isScramble(s1.substr(i), s2.substr(i)))
                return true;
            if( isScramble(s1.substr(0,i), s2.substr(len-i)) && isScramble(s1.substr(i), s2.substr(0,len-i)))
                return true;
        }
        return false;
    }
};

이와 같이 작성한 소스코드는 문제를 정확히 풀이하는 데는 문제가 없었지만, 제출 시 TLE 가 발생했다. 재귀함수의 형태를 취했는데 알파벳의 개수를 비교한 것 외에 가지치기가 전혀 이루어지지 않은 점이 문제로 생각됐다. 추가적인 가지치기가 가능한 방법은, 이미 지나갔던 경로를 재탐색하지 않도록 하는 것이라 생각했다.

 

예를 들어, isScramble("abcde", "caebd") 은 false 를 반환한다. 만약 주어진 문제에서 부분문자열을 나누다가 이와 같은 형태를 마주쳤을 때 굳이 이 문자열을 나눠가며 판정하지 않고 바로 false 를 반환할 수 있다면 문제의 해결에 시간이 단축될 것이다. 하지만 이러한 값을 일일히 넣어줄 수 없으니 주어진 문자열을 가지고 로직을 수행하다 마주치는 경우에 대해 가지치기를 할 필요가 있어 보였다.

 

이를 위해 해시맵을 추가적으로 적용했으며, s1 의 문자열과 s2 의 문자열을 붙여 해시맵의 키로 활용했다. 이를 적용한 소스 코드는 다음과 같다.

 

class Solution {
public:
    map<string, bool> hash;

    bool isScramble(string s1, string s2) {
        if (s1 == s2) {
            return true;
        }
        if (hash.find(s1 + s2) != hash.end()) {
            return hash[s1 + s2];
        }

        int len = s1.length();
        int cnt[26] = {0,};
        for (int i = 0; i < len; i++) {
            cnt[s1[i] - 'a']++;
            cnt[s2[i] - 'a']--;
        }

        for (int i = 0; i < 26; i++) {
            if (cnt[i] != 0) {
                hash[s1 + s2] = false;
                return false;
            }
        }

        bool res = false;
        for (int i = 1; i < len; i++) {
            res = res || isScramble(s1.substr(0, i), s2.substr(0, i)) && isScramble(s1.substr(i), s2.substr(i));
            res = res || isScramble(s1.substr(0, i), s2.substr(len - i)) && isScramble(s1.substr(i), s2.substr(0, len - i));
        }
        hash[s1 + s2] = res;
        return res;
    }
};

위의 코드에서 볼 수 있듯이, 기존의 코드에서 변경된 부분은 해시맵이 적용된 부분이다. 가지치기를 위해 함수 호출 시 해시맵에서 이미 존재하는 경우인 지를 확인하고 이미 해시맵에 저장이 된 경우 저장된 값을 반환한다. 또한 현재 문자열 s1, s2 를 가지고 판정한 결과값을 해시맵에 저장함으로써 추후 가지치기에서 활용할 수 있도록 한다.

 

해시맵을 적용해 일종의 DP 방식을 활용한 결과 TLE 를 피하고 문제를 풀이할 수 있었다.

반응형

주어진 문제는 위와 같다. 2차원 행렬에서 (0, 0) -> (M - 1, N - 1) 까지의 최단 거리를 구하는 문제에 특별한 이동 방식이 추가된 형태이다. 특별한 이동 방식은 체스에서 나이트가 움직이는 방식과 같고 문제에서 정의한 K 만큼 이러한 방식으로 이동할 수 있다.

 

일반적인 BFS 문제는 큐를 사용해 각각의 이동으로 인한 현재 위치를 저장하고 그 위치가 목적지에 도달하면 종료하게 된다. 이 때, 동일한 위치에 대해서 재방문을 할 필요가 없기 때문에 각 위치 (x, y) 의 방문 여부를 저장한다. 이 문제는 이에 더해 그 위치까지 몇 번의 이동 능력을 사용했는 지가 중요하기 때문에 (x, y) 위치 + 이동 능력 사용 횟수 를 기준으로 방문 여부를 체크하면 해결할 수 있다.

 

이러한 풀이로 작성한 소스 코드는 다음과 같다.

 

#include <iostream>
#include <queue>

using namespace std;

const int MAX_SIZE = 200;

struct POINT {
    int r, c;
    int horse, cnt;

    POINT(int r, int c, int horse, int cnt) : r(r), c(c), horse(horse), cnt(cnt) {}
};

int dr[4] = {1, 0, -1, 0}, dc[4] = {0, 1, 0, -1};
int hr[8] = {-1, -2, -2, -1, 1, 2, 2, 1}, hc[8] = {-2, -1, 1, 2, 2, 1, -1, -2};

int N, M, K;
int map[MAX_SIZE][MAX_SIZE];
bool vis[MAX_SIZE][MAX_SIZE][31];

void input() {
    cin >> K;
    cin >> M >> N;

    for (int i = 0; i < N; i++) {
        for (int j = 0; j < M; j++) {
            cin >> map[i][j];
        }
    }
}

int solve() {
    queue<POINT> que;

    que.push(POINT(0, 0, 0, 0));
    while (que.size()) {
        POINT top = que.front();
        que.pop();

        if (top.r == N - 1 && top.c == M - 1) {
            return top.cnt;
        }

        if (vis[top.r][top.c][top.horse])
            continue;
        vis[top.r][top.c][top.horse] = true;

        if (top.horse < K) {
            for (int k = 0; k < 8; k++) {
                int nr = top.r + hr[k], nc = top.c + hc[k];
                if (nr < 0 || nr >= N || nc < 0 || nc >= M)
                    continue;
                if (map[nr][nc] || vis[nr][nc][top.horse + 1])
                    continue;
                que.push(POINT(nr, nc, top.horse + 1, top.cnt + 1));
            }
        }
        for (int k = 0; k < 4; k++) {
            int nr = top.r + dr[k], nc = top.c + dc[k];
            if (nr < 0 || nr >= N || nc < 0 || nc >= M)
                continue;
            if (map[nr][nc] || vis[nr][nc][top.horse])
                continue;
            que.push(POINT(nr, nc, top.horse, top.cnt + 1));
        }
    }

    return -1;
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    input();
    cout << solve();

    return 0;
}

 

solve() 함수에서 기존 BFS 소스 코드와 차이점이 있다면, if (top.horse < K) 로 시작되는 분기문이다. POINT 구조체의 horse 는 현재까지 이동 능력의 사용 횟수이며 이 횟수가 K 보다 작다면 특별한 이동 방식으로 이동할 수 있도록 한다. 또한, bool 타입 3차원 행렬 vis 에서 마지막 인덱스로 horse 값을 사용해 해당 위치까지 가는 동안 사용한 이동 능력의 횟수를 확인해 재방문을 막는다.

'Algorithm > Baekjoon' 카테고리의 다른 글

[BOJ # 11404] 플로이드  (0) 2021.09.23
[BOJ # 1874] 스택 수열  (0) 2021.09.21
<BOJ 2508> 나무 자르기  (0) 2019.02.14
반응형

POJO 구성 방식

5. @Scope 를 붙여 POJO 스코프 지정

@Scope 는 빈 스코프를 지정하는 어노테이션이다. 스프링은 기본적으로 IoC 컨테이너에 선언한 빈마다 정확히 하나의 인스턴스를 생성하고 전체 스코프에서 이를 공유한다. 이것이 스프링에서 사용하는 빈의 기본 스코프인 singleton 이다. 전체 스프링 빈 스포크는 다음과 같다.

Scope Description
singleton IoC 컨테이너당 빈 인스턴스 하나를 생성
prototype 요청마다 빈 인스턴스를 새로 생성
request HTTP 요청당 하나의 빈 인스턴스 생성. (WebApplicationContext 만 해당)
session HTTP 세션당 빈 인스턴스 생성. (WebApplicationContext 만 해당)
globalSession 전역 HTTP 세션당 빈 인스턴스 생성. (PortalApplicationContext 만 해당)

 

다음과 같이 동일한 빈에 대해 getBean 을 각각 요청할 경우, 특별히 스코프를 명시하지 않으면 singleton 으로 인스턴스를 생성한다. 고로 (1) 과 (2) 는 모두 "prod2" 를 화면에 출력하게 된다.

Product prod1 = context.getBean("product", Product.class);
Product prod2 = context.getBean("product", Product.class);

prod1.setName("prod1");
prod2.setName("prod2");

// (1)
System.out.println(prod1.getName());
// (2)
System.out.println(prod2.getName());

 

하지만 클래스에 @Scope 를 붙여 해당 빈의 스코프를 명시하게 되면, 해당 클래스의 인스턴스는 스코프에 따라 인스턴스 생성 규칙을 달리 하게 된다. 다음과 같이 클래스에 prototype 스코프임을 명시하게 되면, 위의 예제에서 (1) 은 "prod1" 을, (2) 는 "prod2" 를 출력하게 된다.

@Component
@Scope("prototype")
public class Product { ... }

 

6. 외부 리소스 데이터 사용

스프링이 제공하는 @PropertySource 를 통해 *.properties 파일을 읽어들일 수 있다. 또한 스프링 Resource 인터페이스에 @Value 를 적용하면 어떤 파일이라도 읽어들일 수 있게 된다.

 

예를 들어, 다음과 같이 정의된 option.properties 파일이 존재한다고 하자.

special.discount=0.1
summer.discount=0.15
endofyear.discount=0.2

위와 같은 파일이 존재할 때, 아래와 같이 구성 클래스에 @PropertySource 를 붙여 사용하고자 하는 properties 파일을 명시하면 endOfYearDiscount 라는 필드에 자동으로 파일 내에 정의된 값이 입력되게 된다. @Value 에서 ${key:defaultValue} 와 같이 주입받고자 하는 값을 지정할 수 있으며, defaultValue 는 만약 값을 찾지 못할 경우에 기본으로 갖게 되는 값을 의미한다.

@Configuration
@PropertySource("classpath:option.properties")
public class ProductConfiguration {

    @Value("${endofyear.discount:0}")
    private double enfOfYearDiscount;
}

 

properties 파일 외에 다른 파일 데이터를 사용하고자 할 경우에는 Resource 인터페이스를 사용할 수 있다.

public class BannerLoader {
	
    private Resource banner;
    
    public void setBanner(Resource banner) {
    	this.banner = banner;
    }
    
    @PostConstruct
    public void showBanner() throws IOException {
    	Files.lines(Paths.get(banner.getURI()), Charset.forName("UTF-8"))
        	.ForEachOrdered(System.out::println);
    }
}

위와 같은 BannerLoader 클래스가 존재한다고 하자. showBanner() 메소드는 등록된 banner 를 읽어 파일의 내용을 차례대로 읽어 한 줄씩 출력하는 기능을 수행한다. 이 메소드에 @PostConstruct 를 붙임으로써, 인스턴스가 생성된 뒤 showBanner() 메소드를 호출하게 된다. 아래와 같이 구성 클래스를 작성하면 스프링은 어플리케이션 실행 시 BannerLoader 의 빈 인스턴스를 생성하므로 어플리케이션 실행 시 자동으로 이 메소드를 호출한다.

@Configuration
@PropertySource("classpath:option.properties")
public class ProductConfiguration {
	
    @Value("classpath:banner.txt")
    private Resource banner;
    
    @Bean
    public BannerLoader bannerLoader() {
    	BannerLoader b1 = new BannerLoader();
        b1.setBanner(banner);
        return b1;
    }
}

@Value("classpath:banner.txt") 를 통해 스프링은 banner.txt 파일을 찾고 이를 banner 필드에 주입하고 Resource 빈 객체로 변환할 수 있게 된다.

 

7. 프로퍼티 파일에서 로케일마다 다른 다국어 메시지 해석

MessageSource 인터페이스에는 리소스 번들 메시지를 처리하는 메소드가 정의되어 있다. ResourceBundleMessageSource 는 MessageSource 구현체로, 로케일별로 분릴된 리소스 번들 메시지를 해석할 수 있다.

 

리소스 번들 메시지를 구분 처리하려면 다음과 같이 ReloadableResourceBundleMessageSource 인스턴스를 자바 구성 파일에 정의한다.

@Configuration
public class Configuration {
	
    @Bean
    public ReloadableResourceBundleMessageSource messageSource() {
    	ReloadableResourceBundleMessageSource messageSource =
        	new ReloadableResourceBundleMessageSource();
        messageSource.setBasenames("classpath:messages");
        return messageSource;
    }
}

빈 인스턴스는 반드시 messageSource 라고 명시해야 ApplicationContext 가 알아서 감지할 수 있다. 위의 예에서는 자바 클래스패스 내에서 이름이 messages 로 시작하는 파일들을 찾도록 설정했다. 이렇게 MessageSource 를 정의하고 미국 로케일에서 텍스트 메시지를 찾으면 messages_en_US.properties 리소스 번들 파일을 제일 먼저 찾게 된다. 이런 이름을 가진 파일이 없거나 메시지를 찾지 못한다면 언어에 맞는 messages_en.properties 를 찾고 이 파일마저 없으면 전체 로케일의 기본 파일인 messages.properties 를 선택한다.

 

다음과 같이 ApplicationContext 를 구성해 getMessage() 메소들 메시지를 해석할 수 있게 된다. 이 메소드에서 첫 번째 인자는 메시지 키, 두 번째는 각 메시지의 매개변수 자리에 끼워넣을 값들, 세 번째는 대상 로케일이다.

public static void main(String[] args) throws Exception {
	
    ApplicationContext context = 
    	new AnnotationConfigApplicationContext(Configuration.class);
        
    String alert = context.getMessage("alert.checkout", null, Locale.US);
    
    System.out.println("Message for alert.checkout is " + alert);
}

위와 같이 messages 파일을 클래스패스에 정의하고 어플리케이션 콘텍스트의 getMessage() 메소드를 사용하면 위처럼 메시지를 가져다 쓸 수 있다. 특히 로케일별 메시지를 적용함으로써 여러 지역에서 서비스를 진행할 때 각 지역 별 메시지를 자동으로 출력하도록 할 수 있을 것 같다.

 

#Reference.

 

스프링 5 레시피(4판)

이 책은 스프링 5에 새로 탑재된 기능 및 다양한 구성 옵션 등 업데이트된 프레임워크 전반을 실무에 유용한 해법을 제시하는 형식으로 다룹니다. IoC 컨테이너 같은 스프링 기초부터 스프링 AOP/A

www.hanbit.co.kr

 

반응형

POJO 구성 방식

1. 자바로 POJO 구성

@Configuration, @Bean 을 붙인 자바 클래스를 만들거나 @Component, @Repository, @Service, Controller 를 붙인 자바 컴포넌트를 구성함으로써 POJO 클래스를 설계한다. IoC 컨테이너는 어노테이션이 붙은 자바 클래스를 탐색해 애플리케이션의 일부인 것처럼 POJO 인스턴스/빈을 구성한다.

 

@Configuration
public class ConfigurationClass {
	
    @Bean
    public BeanClass beanClass() {
    	BeanClass bean = new BeanClass();
        bean.setName("Hello");
        bean.setNick("World");
        return bean;
    }
}

 

@Configuration 어노테이션이 붙은 구성 클래스에서 @Bean 메소드를 통해 빈을 생성한다. 그 후 어노테이션을 붙인 자바 클래스를 스캐닝하기 위해 IoC 컨테이너를 인스턴스화 해야 한다. 스프링에서는 기본 구현체인 BeanFactory 와 이와 호환이 가능한 고급 구현체인 ApplicationContext, 두 가지 IoC 컨테이너를 제공한다.

 

ApplicationContext context = new AnnotationConfigApplicationContext(ConfigurationClass.Class);

 

구성 클래스에 선언된 빈을 BeanFactory / ApplicationContext 에서 가져오려면 유일한 빈 이름을 getBean() 메소드의 인자로 호출한다. getBean() 메소드는 Object 타입을 반환하므로 실제 타입에 맞게 캐스팅하거나, 메소드의 인자로 빈 클래스 타입을 명시할 수 있다. 빈 이름은 기본적으로는 메소드명을 사용하고, @Bean(name = "") 과 같이 이름을 명시한 경우는 어노테이션에 명시한 이름을 사용한다. 해당 클래스의 빈이 하나뿐일 때는 빈 이름을 생략할 수도 있다.

 

BeanClass bean = (BeanClass) context.getBean("beanClass");

// 클래스를 명시함으로써 캐스팅을 하지 않을 수 있음
// bean = context.getBean("beanClass", BeanClass.class);

// 빈 클래스가 하나뿐일 때는 빈 이름을 명시하지 않아도 됨
// bean = context.getBean(BeanClass.class);

 

스프링에는 Persistence, Service, Presentation 이렇게 세 계층이 존재하는데 이를 가리키는 어노테이션이 각각 @Repository, @Service, @Controller 이다. 이 외 범용으로 사용할 수 있는 어노테이션으로 @Component 가 있는데, 쓰임새가 명확하지 않을 때는 @Component 를 사용할 수 있지만 구체적으로 명시하는 편이 용도에 맞는 혜택을 누릴 수 있는 장점이 있다고 한다. (@Repository 는 발생한 Exception 을 DataAccessException 으로 감싸 던지는 등 디버깅 시 유리한 점이 있다.)

 

2. 생성자 호출을 통한 POJO 생성

POJO 클래스에 생성자를 하나 이상 정의한 뒤, 구성 클래스에서 IoC 컨테이너가 사용할 POJO 인스턴스값을 생성자로 설정한다. 그 뒤 IoC 컨테이너를 인스턴스화해 어노테이션을 붙인 자바 클래스를 스캐닝하도록 한다. 그리하면 POJO 인스턴스 / 빈을 애플리케이션 일부처럼 접근할 수 있게 된다.

 

// POJO 클래스 선언 (생성자 정의)
public class Product {
	
    private String name;
    private double price;
    
    public Product(String name, double price) {
    	this.name = name;
        this.price = price;
    }
}

// 구성 클래스 정의
public class ShopConfiguration {
	
    @Bean
    public Product aaa() {
    	Product p = new Product("AAA", 2.5);
        return p;
    }
}

 

3. POJO 레퍼런스와 자동 연결을 통한 상호 작용

POJO / Bean 인스턴스들 사이의 참조 관계는 위와 같이 자바 코드로도 연결을 맺어줄 수 있다. 스프링에서는 필드, Setter 메소드, 생성자, 또는 아무 메소드에 @Autowired 를 붙임으로써 POJO 레퍼런스를 자동으로 연결할 수 있다.

 

@Service
public class TemplateService {

    @Autowired
    private BeanDao beanDao;
    
    // Getter, Setter
}

 

서비스 객체를 생성하는 서비스 클래스틑 실제로 자주 쓰이는 Best Practice 로, DAO 에 대한 직접 호출을 하지 않고 일종의 관문을 두는 것이다. 서비스 객체는 내부적으로 DAO 와 연동해 요청받은 작업을 처리하게 된다.

 

위에서는 필드에 @Autowired 를 적용해 의존성을 주입했지만 다음과 같이 생성자나 메소드와도 자동으로 연결할 수 있다. 또한 컬렉션이나 배열에도 @Autowired 를 적용할 경우 매칭되는 빈을 모두 찾아 자동으로 연결해 준다.

 

public class StringGenerator {
	
    // 필드 주입
    @Autowired
    private PrefixGenerator prefixGenerator;
    
    // 배열형, 컬렉션에 Autowired 적용
    @Autowired
    private PrefixGenerator[] prefixGenerators;
    
    // 생성자 주입
    @Autowired
    public StringGenerator(PrefixGenerator prefixGenerator) {
    	this.prefixGenerator = prefixGenerator;
    }
    
    // Setter 주입
    @Autowired
    public void setPrefixGenerator(PrefixGenerator prefixGenerator) {
    	this.prefixGenerator = prefixGenerator;
    }
}

 

스프링은 기본적으로 @Autowired 를 붙인 프로퍼티에 해당하는 빈을 찾지 못하면 예외를 던진다. 선택적으로 프로퍼티를 적용하고자 할 경우에는 @Autowired(required=false) 와 같이 required 속성값을 지정함으로써 빈을 찾지 못하더라도 그냥 지나치도록 할 수 있다.

 

@Autowired 는 타입을 기준으로 자동으로 빈을 연결하는데, 컨테이너에 호환되는 타입이 여러개 존재할 경우 제대로 연결되지 않을 수 있다. 이러한 문제를 해결하기 위해 @Primary, @Qualifier 를 적용할 수 있다.

 

// @Primary 를 활용
@Component
@Primary
public class PrefixGenerator1 implements PrefixGenerator {
	
    // Methods..
}

// @Qualifier 활용
public class StringGenerator {
	
    @Autowired
    @Qualifier("prefixGenerator1")
    private PrefixGenerator prefixGenerator;
}

 

애플리케이션의 규모가 커질수록 POJO 설정들을 하나의 구성 클래스에 담기 어려워지는데, 보통 POJO 의 기능에 따라 여러 구성 클래스로 나누어 관리한다. 그런데 구성 클래스가 여러 개 공존할 경우 상이한 클래스에 정의된 POJO 를 자동으로 연결하거나 참조하는 것이 쉽지 않다.

 

한 가지 방법은, 구성 클래스가 위치한 경로마다 애플리케이션 컨텍스트를 초기화하는 방법이다. 각 자바 구성 클래스에 선언된 POJO 를 컨텍스트와 레퍼런스로 읽으면 POJO 간 자동으로 연결이 가능하다.

 

ApplicationContext context = 
	new AnnotationConfigApplicationContext(Configuration1.class, Configuration2.class);

 

혹은 @Import 로 구성 파일을 나누는 방법도 존재한다.

 

@Configuration
public class Configuration1 {
	
    @Bean
    private PrefixGenerator prefixGenerator() {
    	// ...
    }
}

@Configuration
@Import(Configuration1.class)
public class Configuration2 {

    @Value("#{prefixGenerator}")
    private PrefixGenerator prefixGenerator;
    
    @Bean
    private StringGenerator stringGenerator() {
    	// ...
    }
}

 

StringGenerator 빈을 설정하기 위해 prefixGenerator 빈을 설정해야 하는데, 이 구성 클래스 내에 존재하지 않고 다른 자바 구성 클래스에 정의되어 있는 경우에 @Import 를 사용한다. @Import 를 붙이면 해당 클래스에 정의한 POJO 를 모두 현재 구성 클래스의 스코프로 가져올 수 있는데, 그런 뒤 위와 같이 @Value 와 SpEL (Spring Expression Language) 를 사용해 외부 구성 클래스에 선언된 빈을 필드에 주입할 수 있다.

 

4. @Resource 와 @Inject 를 사용한 POJO 자동 연결

@Resource 와 @Inject 는 자바 표준(JSR) 에 근거한 해법으로, @Autowired 와 거의 유사하게 사용이 가능하다. 

 

@Resource 는 우선적으로 @Autowired 와 같이 타입을 기준으로 POJO 를 찾아 자동으로 연결한다. 하지만 타입이 같은 POJO 가 여럿일 때 @Autowired 는 가리키는 대상이 모호해져 @Qualifier 를 추가적으로 사용해야 하지만, @Resource 는 기능상 @Qualifier 와 @Autowired 를 합한 것과 같아 자체적으로 대상을 명확하게 지정할 수 있다.

 

@Inspect 는 @Autowired, @Resource 와 같이 타입으로 우선 POJO 를 찾지만 타입이 같은 POJO 가 여럿일 경우에는 다음과 같이 커스텀 어노테이션을 작성해 적용해야 한다.

 

@Qualifier
@Target({ElementType.TYPE, ...})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomAnnotation {
}

 

이 커스텀 어노테이션에 적용된 @Qualifier 는 스프링에서 사용되는 @Qualifier 가 아닌, javax.inject 패키지에 속한 어노테이션이다. 커스텀 어노테이션을 위와 같이 작성한 뒤, 빈 인스턴스를 생성하는 POJO 클래스에 다음과 같이 어노테이션을 붙인 뒤, @Inject 어노테이션과 같이 커스텀 어노테이션을 사용하면 더이상 모호해지는 상황이 발생하지 않는다.

 

@CustomAnnotation
public class PojoClass {
}

public class ServiceClass {

    @Inject @CustomAnnotation
    private PojoClass pojoClass;
}

 

@Autowired, @Resource, @Inject 중 어떤 것을 사용하더라도 결과는 같다. 차이점은 이름을 기준으로 할 경우에 구문이 가장 단순한 @Resource 가 낫고, 타입을 기준으로 할 경우 셋 중 어느 것을 골라도 간편하게 사용이 가능하다.

 

* 개인적인 생각을 덧붙이자면, 어떤 어노테이션을 쓰느냐는 개인의 마음이라고 볼 수 있겠지만 프로젝트에서 사용함에 있어 하나로 통일해서 사용해야 할 필요는 있을 것 같다. 기능적으로 여러 개를 혼용해 써도 문제가 될 것 같지는 않지만, 추후에 코드를 봤을 경우에 어노테이션이 뒤죽박죽 사용되어 있다면 불필요한 혼란을 가져올 수 있을 것 같다.

 

#Reference.

 

스프링 5 레시피(4판)

이 책은 스프링 5에 새로 탑재된 기능 및 다양한 구성 옵션 등 업데이트된 프레임워크 전반을 실무에 유용한 해법을 제시하는 형식으로 다룹니다. IoC 컨테이너 같은 스프링 기초부터 스프링 AOP/A

www.hanbit.co.kr

 

+ Recent posts