잘못된 내용에 대해서는 댓글 부탁드립니다.

 

 

 

Concurrency


하나의 코어가 여러 프로세스를 번갈아가며 실행하는 것을 의미한다. 이는 사용자에게 동시에 실행되는 것처럼 보이게 만드는 효과를 가지며, 단위 시간 내에 더 많은 일을 처리한다.

 

프로세스 간의 컨텍스트 스위칭이 발생한다.

 

 

Concurrency의 장단점


장점

  • CPU의 처리량이 증가한다.
  • 자원의 활용도가 증가한다.
  • 프로세스 간의 대기시간이 감소된다.

단점

  • Context Switching에 대한 Overhead가 발생한다.

 

 

 

Parallelism


하나의 프로세스를 분할하여 처리

 

여러 개의 코어가 하나의 프로세스의 작업을 분할하여 처리하는 것을 의미할 수 있다. 이는 내부적으로 동작하는 스레드의 개수만큼 CPU에 할당할 수 있음을 의미한다.

 

화면을 랜더링 하는 스레드, 계산을 진행하는 스레드, 서버와 통신하는 스레드 등...

 

 

한 번에 여러 프로세스를 실행

 

이와 같이 각 코어가 별개의 프로세스를 동작시킴으로써 단위 시간 내에 여러 프로세서를 동작시키는 것을 의미할 수도 있다.

 

 

Parallelism의 장단점


장점

  • 하나의 프로세스를 분할하여 여러 작업으로 처리할 수 있다.
  • 하나의 작업에 대해 가용 자원을 더 많이 할당할 수 있다.
  • 여러 프로세스에 대해서도 동시에 수행할 수 있다.

단점

  • 단일 코어 방식보다 어려운 작성 방식을 가진다.
  • 프로세스 분할 처리시 발생하는 추가 비용이 더 크다. 데이터 전송, 동기화, 통신, 전환 등
  • 각각의 시스템 아키텍처에 맞게 알고리즘 로직의 조정이 필요하다.

 

 

 

Parallel and Concurrency


여러 개의 코어에서 여러 프로세스들을 번갈아 실행하는 상황을 의미한다.

 

물리적인 개념(Parallel)과 논리적인 개념(Context Switching)이 연계된 것이다.

 

 

참고 자료

'Programming' 카테고리의 다른 글

Java의 Reflection API와 성능 이슈?  (0) 2021.02.02
G1 GC  (0) 2021.01.26
Service에 @Transactional 을 적용한다면?  (0) 2021.01.18
Spring Auto-Configuration Condition Annotations  (0) 2021.01.12
Spring Boot의 Auto Configuration!  (0) 2021.01.12

서비스 메서드에 Transcational을 사용하였을 때와 사용하지 않았을 때, 흐름을 정리해보았습니다.

 

스프링 @Transaction 미적용 (JDBC API - Local Transaction)

기본적으로 JDBC의 트랜잭션은 하나의 Connection Instance를 생성하고 통신하며 종료하는 흐름과 같이 동작하게 된다.

즉 코드에 존재하는 DAO 로직들은 각각의 트랜잭션 안에서 연산을 진행하게 되는 것이다.

 

이때의 트랜잭션을 로컬 트랜잭션이라고 한다.

 

해당 메서드 내에서는 3개의 트랜잭션이 동작하며, 이는 내부에서 하나의 DAO 로직이 실패하더라도 다른 로직들은 성공하고 반영될 수 있는 상태임을 의미한다.

 

각각의 로직들은 Connection Pool에서 리소스 전달받아 새로운 Connection을 생성하며, Auto-Commit을 진행한다.

 

트랜잭션을 적용했다면?

단순하게 생각해본다면, 여러 질의를 포함하는 트랜잭션을 구성하기 위해서 하나의 커넥션을 생성하고 Auto-commit을 false 처리한 뒤 이 커넥션을 재사용을 하면 될 것 같다.

Spring에서는 이를 구현하는 방법을 Transaction Synchronization이라고 한다.

 

Transaction Synchronization?

개념 정립을 위하여 만든 이미지이므로 실제 내용과는 다를 수 있습니다.

트랜잭션을 시작하기 위해 사용할 Connection 객체를 저장소 역할을 하는 Connection Holder에 보관하고, 이후 호출 로직들에 대해서 매번 Connection을 생성하고 사용하는 것이 아니라 해당 Connection만을 꺼내어 재사용하게끔 한다.

 

트랜잭션 동기화를 적용한 JDBC Template work flow

  • DAO의 호출을 위한 connection을 트랜잭션 경계 상단에서 생성한다.
  • 해당 connection 객체를 TransactionSynchornizationManager 내부의 참조 변수인 connectionHolder 객체에 저장한다.
  • connection의 Auto-commit 설정 값을 false로 설정한다.
  • DAO의 메서드가 호출되면 우선 Manager 내부의 Holder 객체에 connection이 있는지 확인한다.
  • 저장되어 있는 Connection을 가져오고 Statement 객체를 생성하여 쿼리를 전송한다. 그리고 연산 종료 시 해당 connection을 종료시키지 않고 열어둔다
  • 위와 같은 연산을 진행하며 Runtime Exception이 발생하면 connection 객체의 RollBack을 실행하고 그렇지 않은 경우 commit을 실행한다.

 

Spring Transcation Synchronization Interface

 

TransactionSynchronizationManager

스프링에서 제공하는 Transaction Synchronization 용 Manager Class이다.

선언적 트랜잭션을 이용하게 될 경우 트랜잭션 경계의 맨 첫 부분에서 initSynchronization()를 호출하여 트랜잭션 동기화 작업을 진행한다.

private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
            new NamedThreadLocal<>("Transaction synchronizations");

/**
     * Return if transaction synchronization is active for the current thread.
     * Can be called before register to avoid unnecessary instance creation.
     * @see #registerSynchronization
     */
    public static boolean isSynchronizationActive() {
        return (synchronizations.get() != null);
    }

    /**
     * Activate transaction synchronization for the current thread.
     * Called by a transaction manager on transaction begin.
     * @throws IllegalStateException if synchronization is already active
     */
    public static void initSynchronization() throws IllegalStateException {
        if (isSynchronizationActive()) {
            throw new IllegalStateException("Cannot activate transaction synchronization - already active");
        }
        logger.trace("Initializing transaction synchronization");
        synchronizations.set(new LinkedHashSet<>());
    }

해당 작업은 해당 스레드 내부에서만 사용될 값을 저장하는 ThreadLocal 객체에 LinkedHashset 컬랙션 객체를 저장하고, 이 안에는 트랜잭션 동기화 설정과 관련된 필드를 가지는 TransactionSynchronization 타입의 객체를 저장한다.

 

 

참고 자료

내용 추가 : resultType을 entity, dto 등의 Value Object으로 지정할 경우 resultMap을 생성한다.

 

 

마이바티스와 롬복을 같이 사용하는 것은 처음이다보니 사소한 실수를 통해 여러 예외를 만나게 되는 것 같다.

User 도메인을 개발하기 시작했기에 엔티티를 작성하고 테스트 데이터베이스 스키마와 데이터를 추가한 뒤 MapperTest를 통해 조회 테스트를 진행하던 중 문제가 발생하였다.

Caused by: org.apache.ibatis.exceptions.PersistenceException: 
### Error querying database.  Cause: java.lang.IndexOutOfBoundsException: Index 9 out of bounds for length 9
### The error may exist in file [C:\Users\serrl\Desktop\Mentoring\Somaeja\out\production\resources\mybatis\mapper\user.xml]
### The error may involve com.somaeja.user.mapper.UserMapper.findByAll
### The error occurred while handling results
### SQL: SELECT *         FROM USER
### Cause: java.lang.IndexOutOfBoundsException: Index 9 out of bounds for length 9
    at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:149)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:426)
    ... 82 more
Caused by: java.lang.IndexOutOfBoundsException: Index 9 out of bounds for length 9
    at java.base/jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:64)
    at java.base/jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:70)
    at java.base/jdk.internal.util.Preconditions.checkIndex(Preconditions.java:248)
    at java.base/java.util.Objects.checkIndex(Objects.java:372)
    at java.base/java.util.ArrayList.get(ArrayList.java:458)
    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createUsingConstructor(DefaultResultSetHandler.java:708)
    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createByConstructorSignature(DefaultResultSetHandler.java:693)
    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createResultObject(DefaultResultSetHandler.java:657)
    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createResultObject(DefaultResultSetHandler.java:630)
    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.getRowValue(DefaultResultSetHandler.java:397)
    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValuesForSimpleResultMap(DefaultResultSetHandler.java:354)
    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValues(DefaultResultSetHandler.java:328)
    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSet(DefaultResultSetHandler.java:301)
    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSets(DefaultResultSetHandler.java:194)
    at org.apache.ibatis.executor.statement.PreparedStatementHandler.query(PreparedStatementHandler.java:65)
    at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:79)
    at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:63)
    at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:325)
    at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156)
    at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109)
    at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:89)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:147)
    ... 88 more

난데없이 IndexOutOfBoundsException가 발생하여 (발생할 꺼리도 없는데????) 약간 헤매긴 하였는데 몇몇 요소를 검증하니 찾을 수 있었다.

 

문제는 바로 Entity 객체였다. (사실은 내 문제다..!)

@Builder
@Getter
@ToString
public class User {

    private final Long id;
    private final Long locationId;
    private final String nickName;
    private final String password;
    private final String email;
    private final String phoneNumber;
    // 계정 권한
    private final String role;
    private final LocalDateTime createdDate;
    private final LocalDateTime modifyDate;

    // Inner Join 을 통해 가져오는 데이터
  // 이것이 누락되었다..!
    private final String cityCountryTown;

}
<select id="findByAll" resultType="com.somaeja.user.entity.User">
    SELECT *
    FROM USER
</select>

Entity를 별도의 설정자 없이 빌더 패턴으로 생성하다보니 모든 필드 값을 final로 설정하였었는데, 테스트를 작성하기 전에 간단하게 작성했던 xml 에서 Join을 통해 가져올 데이터를 누락시키고 있었기에 해당 Entity 생성 로직 자체가 실패한 것이였다.

<select id="findByAll" resultType="com.somaeja.user.entity.User">
    SELECT USER.*, LOCATION.CITY_COUNTRY_TOWN as location
    FROM USER INNER JOIN LOCATION
    ON USER.LOCATION_ID = LOCATION.LOCATION_ID
</select>

이렇게 변경하여 해당 필드 데이터로 가져옴으로써 해결하게 되었다.

내부 로직을 보지않아 정확히 알 수는 없지만 요소가 빠짐으로써 발생하는 Exception이라면 IndexOutOfBoundsException이 아니라 IligalArgumentException을 사용하는 것이 더 좋지 않았을까 라는 생각이 들었다. (물론 내부 로직에 그럴만한 이유가 있겠지...)

 

다른 상황?

다른 문제로도 해당 예외가 발생할 수 있는데, 이는 resultType에서 객체를 사용할 때와 resultMap을 사용할 때 나타나는 경우이다.

 

 

예시 Entity

@Builder
@Getter
public class Account {

        private Long id;
    private String name;
    private String nickname;
    private String phoneNumber;
    private Boolean phoneVerified;

}

위와 비슷하게 빌더 패턴을 이용해서만 Entity를 생성하고 제공한다고 가정한다.

<resultMap id="AccountResultMap" type="....Account">
        <result property="id" column="id"/>
    <result property="name" column="name"/>
    <result property="nickname" column="nickname"/>
    <result property="phoneNumber" column="phone_number"/>
    <result property="phoneVerified" column="phone_verified"/>
</resultMap>

<select id="findById" parameterType="long" resultMap="AccountResultMap">
        SELECT *
        FROM Account
        WHERE id = #{accountId}
</select>

이러한 resultMap을 정의하고 단순하게 id를 통해 조회하는 SELECT 쿼리를 작성하였다.

이 경우에도

Caused by: org.apache.ibatis.exceptions.PersistenceException: 
### Error querying database.  Cause: java.lang.IndexOutOfBoundsException: Index 5 out of bounds for length 5
### The error may exist in file [mybatis\mapper\account.xml]
### The error may involve ...mapper.AccountMapper.findById
### The error occurred while handling results
### SQL: SELECT *         FROM Account         WHERE id = #{accountId}
### Cause: java.lang.IndexOutOfBoundsException: Index 5 out of bounds for length 5

이러한 에러가 발생할 것이다. 이는 resultMap의 특성 때문인데, 해당 기능을 사용할 경우 (+resultType 을 사용할 때 객체를 넘겨줄 경우 동일하다.) Mybatis가 미리 해당 인스턴스를 생성하게 된다. 하지만 위의 엔티티는 모든 인자가 포함된 생성자 (Builder) 만이 존재하기 때문에 인스턴스를 생성할 수 없어 문제가 발생하게 되는 것이다.

이 것을 해결하기 위해서는 인자가 없는 생성자를 추가하면 된다.

@Builder
@Getter
@NoArgsConstructor <-- 이것을 추가해주면 된다.
public class Account {

        private Long id;
    private String name;
    private String nickname;
    private String phoneNumber;
    private Boolean phoneVerified;

}

이렇게 한다면 앞서 언급한 특징에 의해 발생한 문제일 경우 해결될 것이다.

 

 

 

@Builder에 대한 토막글

Finally, applying @Builder to a class is as if you added @AllArgsConstructor(access = AccessLevel.PACKAGE) to the class and applied the @Builder annotation to this all-args-constructor. This only works if you haven't written any explicit constructors yourself.

 

참고 자료

 

Auto-Configuration Conditions

주어지는 여러가지 조건들을 이용하여 Spring Application 실행시 사용할 Bean Definition을 선택할 수 있도록하는 Annotation들의 집합이다.

 

  • @ConditionalOnClass

    어노테이션의 인자로 받은 클래스가 존재할 때, 해당 주석이 적용된 Configuration class의 Java기반 Bean Definition들을 등록한다.

 

  • @ConditionalOnMissingClass

    어노테이션의 인자로 받은 클래스가 존재하지 않을 때, 해당 주석이 적용된 Configuration class의 Java기반 Bean Definition들을 등록한다.

 

  • @ConditionalOnBean

    어노테이션의 인자로 받은 Bean ID가 존재할 때 해당 주석이 적용된 Configuration class의 Java기반 Bean Definition들을 등록한다.

 

  • @ConditionalOnMissingBean

    어노테이션의 인자로 받은 Bean ID가 존재하지 않을 때 해당 주석이 적용된Configuration class의 Java기반 Bean Definition들을 등록한다.

 

  • @ConditionalOnProperty

    application.properties 구성 정보와 값에 따라 특정한 Bean을 등록하는데 사용된다.

    ex) Prod 용 DataSource, Test 용 DataSource 등

    • value

      속성의 이름을 나타내는 필드이다. (ex) lob)

    • prefix

      각 속성에 적용되는 접두사를 정의한다. 기본 값이 지정되지 않으면 .을 포함한다.

    • name

      검증될 속성의 이름을 나타낸다. (prifix + value ex) : .lob)

    • havingValue

      해당 속성에 대한 예상 값을 나타내며, 값이 지정된 경우 다른 값이여야 한다. 기본적으로 빈 문자열 로 정의된다.

    • matchIfMissing

      해당 속성이 설정되지 않은 경우 정의한 조건을 사용할지에 대해 정의한다. 기본적으로 false로 정의되어 있다.

 

  • @ConditionalOnResource

    특정한 리소스가 존재할 때 해당 Class의 Definition을 사용하게 한다.

    ex) (resources = "classpath:application.properties")

 

  • @ConditionalOnWebApplication

    현재 Application이 Web Application이라면 해당 Definition을 사용한다.

 

  • @ConditionalOnNotWebApplication

    현재 Application이 Web Application이 아니라면 해당 Definition을 사용한다.

 

  • @ConditionalExpression

    SpEL 표현식을 통해 작성된 검증로직이 true인 경우 해당 Definition을 사용한다.

 

  • @Conditional

    Custom된 지정 조건 Class를 만든 뒤 해당 어노테이션에 인자로 넘긴 뒤 true인 경우 해당 Definition을 사용한다.

 

 

Conditional의 동작

해당 어노테이션들의 동작도 ConfigurationClassParser를 통해 이루어진다.

 

  1. ConfigurationClassParser 의 process 메서드 호출

  2. 추가가 될 수 있는 모든 그룹(AutoConfiguration.class)에 대한 정보를 저장해온다.

  3. processGroupImports()가 호출되고 앞서 저장된 정보들을 순회하면서 각각의 ConfigurationClass 정보를 가져온 뒤 processImports() 메서드를 호출한다.

  4. 저장된 정보들은 Configuration Class들 이기 때문에 processConfigurationClass() 를 호출한다.

  5. 최상단에 shouldSkip 메서드를 호출한 뒤 몇몇 조건을 검증한다.

    1. MetaData가 Null 이거나 Conditional class 유형의 어노테이션이 없는 경우

      → 아무것도 하지않고 false를 Return 한다.

    2. ConfigurationPhase phase가 Null 인 경우

      해당 MetaData가 AnnotationMetadata이고 Annotation이라면, 혹은 Method 단위에 Annotation이 붙어있다면 shouldSkip 메서드를 호출하며 ConfigurationPhase를 PARSE_CONFIGURATION로 설정한다.

       

      위의 조건이 아니라 Interface이거나 메서드 단위의 Annotation이 작성되지 않았다면 shouldSkip 메서드를 호출하며 REGISTER_BEAN로 설정한다.

       

       

      ConfigurationPhase 의 속성 정보

      • PARSE_CONFIGURATION

        @Configuration Annotation을 파싱할 때 작성된 조건식을 평가한다.

      • REGISTER_BEAN

        ConfigurationClassBeanDefinitionReader가 Bean을 등록할 때 조건식을 평가한다.

  6. 이후 MetaData 정보에서 Condition 정보가 존재하는지 확인하고 리스트에 저장한다.

  7. 저장된 정보를 순회하며, ConfigurationPhase requiredPhase 만든 뒤 조건을 검증한다.

    1. requiredPhase 가 Null 이거나 (||) 메서드를 호출될 때 넘겨받은 phase 속성 값과 같고 condition 검증 조건이 false인 경우 true를 반환한다. true를 전달받은 경우 5번 절차에 조건식을 만족함으로 메서드 실행이 종료된다.

    이때 호출되는 matches 메서드가 Condition 인터페이스를 구현한 SpringBootCondition의 matches 메서드이다.

    해당 메서드는 ConditionContext 객체와 AnnotationType의 Metadata를 넘기면서 하위 어노테이션들의 getMatchOutcome()을 호출한다. 해당 메서드는 정해진 조건식을 검증하고 그에 따른 결과로 boolean 값을 전달한다.

 

 

 

참고자료

주의 : 틀린 내용이 있을 수 있습니다..! 내용을 보완 중에 있습니다.

 

SpringBootConfiguration?

개발자의 편의를 위해 EnableConfiguration과 SpringBootConfiguration, 그리고 Component Scan을 합쳐 하나로 제공한다.

// 기존에 작성하던 방식을 좀더 간편하게 하였다.
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public class ExampleApplication {

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

// 동일하게 동작한다.
@SpringBootApplication
public class ExampleApplication {

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

 

EnableAutoConfiguration?

Spring Boot Application에 존재하는 특정한 패키지에 포함된 jar 파일과 Bean Metadata들을 CLASSPATH를 통해 접근하여 Bean들을 자동 구성하는 애노테이션이다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

}

Spring-Boot-Stater Dependency들을 추가하게 되면 해당 정보들이

 

org.springframework.boot:spring-boot-autoconfigure > META-INF > spring.factories

 

에 포함되며 별도로 제외할 basePackage, class 정보를 적용하지 않는 이상 모두 자동으로 등록하게 된다.

 

 

spring.factories

# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener

# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
.....

내부에 정의된 클래스 패스들을 따라가게 되면 @ConditionalXXX를 통해 각각의 조건을 검증하여 통과하는 경우에 등록하는 방식으로 선언되어 있다.

 

 

ex) DispatcherServletAutoConfiguration

@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
public class DispatcherServletAutoConfiguration {

    /**
     * The bean name for a DispatcherServlet that will be mapped to the root URL "/".
     */
    public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet";

    /**
     * The bean name for a ServletRegistrationBean for the DispatcherServlet "/".
     */
    public static final String DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = "dispatcherServletRegistration";

  // DispatcherServletRegistrationConfiguration...

    // DefaultDispatcherServletCondition...

    // DispatcherServletRegistrationCondition...
}

 

EnableAutoConfiguration 흐름 요약

Context가 초기화되면 등록된 BeanPostProcessor들을 이용하여 BeanDefinition들을 읽어오게 됩니다. 이때 AutoConAutoConfigurationPackage와 연결된 AutoConfigurationPackage.class의 정적 클래스인 registrar가 Spring.factory파일의 클래스 패스 정보를 통해 Configuration Class들을 모두 가져와 저장하게 되고, 이것이 ConfigurationClassBeanDefinitionReader를 통해서 읽어져 온 뒤 ConfigurationClassParser에서 Condition 구현체를 통한 조건 검증을 진행하고 통과한 Bean Definition에 대해서만 ImportStack에 추가하여 BeanPostProcessor가 해당 정보를 싱글톤 형식의 Bean으로 등록하게 됩니다. 

 

 

@Configuration

해당 어노테이션이 작성된 클래스가 Java Config 기반의 Bean Definition 으로 사용되는 것임을 나타낸다.

내부적으로 @Component라는 Meta-Annotation이 정의되어 있으므로 ComponentScan을 통해 탐색되어 읽어 들일 수 있으며, 이는

  1. ConfigurationClassPostProcessor의 postProcessBeanDefinitionRegistry()메서드 호출
  2. 내부의 processConfigBeanDefinitions() 메서드를 호출한 뒤 ConfigurationClassParser 를 통해 읽어 들여진 Configuration Class들을 ConfigurationClass 타입으로 파싱 한다.
  3. 해당 정보를 ConfigurationClassBeanDefinitionReader의 loadBeanDefinitions() 메서드에 전달하여 순회한다.
  4. 순회되는 정보들을 loadBeanDefinitionsForConfigurationClass() 메서드를 통해 정보를 읽어 들어와 Bean Definition으로 등록한다.
  5. 이후 ConfigurationClass 객체들을 alreadyParsed에 저장하고 다음 로직을 진행한다.

의 흐름을 가지고 등록되게 된다.

 

 

@EnableAutoConfiguration에 포함된 AutoConfigurationImportSelector.class, @AutoConfigurationPackage의 흐름 따라가 보기

 

Import(AutoConfigurationImportSelector.class)

META-INF/spring.factories에 존재하는 정보들을 가져와 등록하는 클래스이다.

  1. Main 메서드가 호출되고 run 메서드가 실행된다.

  2. run 메서드 실행 중 refreshContext() 메서드가 ConfigurableApplicationContext 타입의 객체를 전달받으며 호출된다.

  3. 넘겨받은 객체 타입을 검증하는 refresh(ApplicationContext) 메서드에 전달하여 확인하고 해당 객체 타입의 인자가 전달되지 않는 refresh() 메서드를 호출한다.

  4. 메서드 내부적으로 invokeBeanFactoryPostProcessors(BeanFactory); 를 호출한다. 인자로 전달받은 beanFactory에서 PostProcessor 개수를 전달받은 뒤 순회하면서 해당 정보를 ArrayList인 currentRegistryProcessors에 저장한다.

  5. currentRegistryProcessors를 정렬하고 해당 리스트와 beanFactory를 BeanDefinitionRegistry로 Casting 한 다음 invokeBeanDefinitionRegistryPostProcessors() 메서드에 전달한다.

  6. 내부적으로 전달받은 ArrayList를 순회하며 각각의 PostProcessor 들을 등록하는 절차를 진행하는 중 postProcessBeanDefinitionRegistry → processConfigBeanDefinitions에 regisrar 정보를 넘긴다.

  7. processConfigBeanDefinitions 내부에서 BeanDefinitionHolder 정보를 담는 ArrayList인 configCandidates 객체를 생성한 뒤 해당 객체의 요소를 추가하는 작업을 진행한다.

  8. → 전달받은 registry가 가지고 있는 DefinitionNames 들을 가져와 순회하면서 BeanDefinition 정보와 Bean Name을 꺼내오고 그것을 저장하는 BeanDefinitionHolder를 생성하여 저장한다.

  9. configCandidates 객체를 정렬한 뒤 ConfigurationClassParser를 생성하고 정렬된 객체를 Set으로 변환한 다음 parse() 메서드의 인자로 넘긴다.

  10. parse() 메서드 내부에서는 전달받은 Set를 순회하며 가져온 BeanDefinition 정보를 instanceof를 통해 Sub Class Type을 검증한 뒤 해당 정보에 맞게 캐스팅하여 BeanName과 함께 parse()를 호출하며 인자로 넘긴다.

  11. deferredImportSelectorHandler의 process()가 호출된다.

  12. 해당 메서드는 deferredImportSelectors 정보를 가져와 정렬한 뒤 forEach를 통해 순회하며 DeferredImportSelectorGroupingHandler register()를 호출하여 해당 정보에 포함된 Annotation Meta 정보와 ConfigurationClass 정보, deferredImport 정보를 호출된 객체 내부에 존재하는 Map에 저장한다.

    List<ConfigurationClassParser.DeferredImportSelectorHolder> deferredImports 
            = this.deferredImportSelectors;
    
    ConfigurationClassParser.DeferredImportSelectorGroupingHandler handler 
            = ConfigurationClassParser.this.new DeferredImportSelectorGroupingHandler();
    
    deferredImports.sort(ConfigurationClassParser.DEFERRED_IMPORT_COMPARATOR);
    deferredImports.forEach(handler::register);
    handler.processGroupImports();
    
    private class DeferredImportSelectorGroupingHandler {
    
            private final Map<Object, DeferredImportSelectorGrouping> groupings = new LinkedHashMap<>();
    
            private final Map<AnnotationMetadata, ConfigurationClass> configurationClasses = new HashMap<>();
    
            public void register(DeferredImportSelectorHolder deferredImport) {
                Class<? extends Group> group = deferredImport.getImportSelector().getImportGroup();
                DeferredImportSelectorGrouping grouping = this.groupings.computeIfAbsent(
                        (group != null ? group : deferredImport),
                        key -> new DeferredImportSelectorGrouping(createGroup(group)));
                grouping.add(deferredImport);
                this.configurationClasses.put(deferredImport.getConfigurationClass().getMetadata(),
                        deferredImport.getConfigurationClass());
            }
  13. 저장된 정보를 가지고 processGroupImports() 메서드를 호출한다.

  14. 앞서 저장된 Map을 순회하면서 getImports() 메서드를 호출한다.

  15. getImports() 메서드는 전달받은 DeferredImportSelectorHolder 타입 객체 정보를 순회한다.

  16. AutoConfigurationImportSelector의 process()를 호출하면서 DeferredImportSelectorHolder의 ConfigurationClass의 Metadate 정보와 importSelector를 전달한다.

  17. 주어진 importSelector를 AutoConfigurationImportSelector로 캐스팅하여 getAutoConfigurationEntry() 메서드를 호출한 다음 AutoConfigurationEntry 타입 객체인 autoConfigurationEntry에 저장한다.

  18. 저장한 entry의 MetaData를 가져와 ConfigurationClass 타입의 객체를 선언하여 저장한다.

  19. processImports() 메서드를 호출하고 주어진 ConfigurationClass 객체를 importStack이 상속받은 ArrayDeque에 해당 정보를 저장한다.

  20. 전달받은 Collection importCandidates 향상된 For문으로 순회한다.

  21. 해당 정보는 ImportSelector 또는 ImportBeanDefinitionRegistrar가 아닌 Configuration 정보이기에 해당 정보를 importStack의 LinkedMultiValueMap에 저장하게 된다.

    → 이때 저장되는 정보는 위에서 주어진 ConfigurationClass를 SourceClass로 필터를 거쳐 Converting 된 객체의 Metadata와 enrty의 포함된 Import Class 컬랙션이다.

  22. importCandidates의 요소를 ConfigClass로 변환하고 grouping에 저장되어 있던 exclusionFilter를 전달한 뒤 SourceClass로 컨버팅하고 doProcessConfigurationClass()를 호출한다.

  23. Bean, ComponentScan 등의 어노테이션들 정보가 포함되어 있는지 확인하여 각각의 어노테이션에 맞게 해당 정보를 로딩하거나 파싱 한 뒤 Bean을 생성한다.

 

@AutoConfigurationPackage?

자동 구성 패키지 정보를 저장하는 AutoConfigurationPackages 클래스의 정보를 가져오는 어노테이션이다.

 

AutoConfigurationPackages 클래스는 자동 구성 패키지 내의 정의된 Meta-annotation들을 기반으로 정보들을 가져온 뒤 별도의 리스트를 통해 (이후에 참조될) 패키지 이름들을 저장하는 로직을 가지고 있다.

 

해당 로직을 통해 저장된 패키지 정보들은 이후 beanDefinition으로 저장되게 되는데 이는 Import 된 AutoConfigurationPackages.Registrar.class를 통해 이루어진다.

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {

        @Override
        public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
            register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
        }

        @Override
        public Set<Object> determineImports(AnnotationMetadata metadata) {
            return Collections.singleton(new PackageImports(metadata));
        }
}

public static void register(BeanDefinitionRegistry registry, String... packageNames) {
    if (registry.containsBeanDefinition(BEAN)) {
            BasePackagesBeanDefinition beanDefinition = (BasePackagesBeanDefinition) registry.getBeanDefinition(BEAN);
            beanDefinition.addBasePackages(packageNames);
    }
    else {
            registry.registerBeanDefinition(BEAN, new BasePackagesBeanDefinition(packageNames));
    }
}

PackageImports(AnnotationMetadata metadata) {
        AnnotationAttributes attributes = AnnotationAttributes
                .fromMap(metadata.getAnnotationAttributes(AutoConfigurationPackage.class.getName(), false));
        List<String> packageNames = new ArrayList<>(Arrays.asList(attributes.getStringArray("basePackages")));
        for (Class<?> basePackageClass : attributes.getClassArray("basePackageClasses")) {
                packageNames.add(basePackageClass.getPackage().getName());
        }
        if (packageNames.isEmpty()) {
                packageNames.add(ClassUtils.getPackageName(metadata.getClassName()));
        }
        this.packageNames = Collections.unmodifiableList(packageNames);
}

 

EnableAutoConfiguration와 관련된 어노테이션들

  • @AutoConfigureOrder

    ApplicationContext에 전달될 자동 구성 클래스 순서를 정의하는 어노테이션이다.

    각 Bean의 의존 관계와 DependsOn 어노테이션을 통해 내부적으로 등록 순서가 결정되며 Bean이 만들어지는 것과 주입하는 순서에 대한 속성과는 별개의 어노테이션이다.

  • @AutoConfigureAfter

    인자로 주어진 자동 구성 클래스가 적용된 뒤에 해당 어노테이션이 적용된 자동 구성 클래스를 적용하게끔 하는 어노테이션이다.

  • @Import

    다른 @Configuration 이 적용된 클래스의 Bean Definition을 명시적으로 컨테이너나 구성 클래스에 적용할 수 있도록 한다.

    ComponentScan 어노테이션과 유사한 동작 방식을 가지지만, 다수의 구성 클래스를 적용하기 위해서는 모두 명시적으로 작성하여야 된다.

    기본적으로 사용하는 이유는 여러 설정을 하나의 클래스에 작성하지 않고 유사성을 따라 분리하여 정의하고 하나의 구성 클래스에서 정보를 집계하여 사용하기 위함이다.

      @Configuration
      @Import({ AConfig.class, BConfig.class })
      public class GroupConfiguration {
      }

 

Spring Boot가 설정해주는 것들 몇 가지 짚어보기

 

DataSource 자동 초기화

  1. Main 메서드가 호출되고 run 메서드가 실행된다.

  2. run 메서드 실행 중 refreshContext() 메서드가 ConfigurableApplicationContext 타입의 객체를 전달받으며 호출된다.

  3. 넘겨받은 객체 타입을 검증하는 refresh(ApplicationContext) 메서드에 전달하여 확인하고 해당 객체 타입의 인자가 전달되지 않는 refresh() 메서드를 호출한다.

  4. 메서드 내부적으로 invokeBeanFactoryPostProcessors(BeanFactory); 를 호출한다. 인자로 전달받은 beanFactory에서 PostProcessor 개수를 전달받은 뒤 순회하면서 해당 정보를 ArrayList인 currentRegistryProcessors에 저장한다.

  5. currentRegistryProcessors를 정렬하고 해당 리스트와 beanFactory를 BeanDefinitionRegistry로 Casting 한 다음 invokeBeanDefinitionRegistryPostProcessors() 메서드에 전달한다

  6. 내부적으로 전달받은 ArrayList를 순회하며 각각의 PostProcessor 들을 등록하는 절차를 진행한다. postProcessBeanDefinitionRegistry → processConfigBeanDefinitions → ConfigurationClassBeanDifinitionReader에 정보를 넘겨 생성한 뒤 loadBeanDefinitions 메서드를 호출한다.

  7. 넘겨받은 Set를 순회하며 loadBeanDefinitionsForConfigurationClass를 호출한다. 해당 메서드는 loadBeanDefinitionsFromRegistrars()를 호출하여 registrar 들의 registerBeanDefinitions() 메서드를 호출하는 절차를 진행한다.

  8. DataSourceInitializationConfiguration 내부에 Regisrar의 registerBeanDefinitions()를 호출하고 해당 로직에서 dataSourceInitializerPostProcessor이라는 이름을 가진 PostProcessor 가 존재하지 않는 경우 DataSourceInitializerPostProcessor.class을 등록하는 로직을 실행한다.

  9. 이후 Bean 이 등록되어 초기화 절차를 진행하는데 이때 afterPropertiesSet() 메서드를 호출하여 createSchema()를 통해 DB의 Schema를 등록하는 절차를 진행한다.

  10. 위의 로직이 성공하면 initialize()를 호출하여 구성된 Schema에 data.sql 정보를 읽어 등록한다.

  11. sql 파일들을 통해 정상적으로 DB가 구성되고 사용할 수 있게 된다.

 

내용을 계속 추가하고 있습니다.

 

 

 

Spring vs Spring Boot

[의존성 관리와 설정을 자동으로 해준다! - Starter]

 

스프링 부트는 스프링 프레임워크의 경량화를 위해 분리된 수많은 Jar 파일을 필요에 맞게 등록하고, 관련 설정을 선언하는 절차없이 기본 설정값으로 제공되는 @Configuration Class들을 통해 빠른 실행을 가능케 하고, Bean에 대한 Builder를 통해 사용자 정의도 쉽게 할 수 있게 하여 빠른 개발 환경 구성을 지원하기 위해 만들어진 것입니다.

 

내장 톰켓에 대해서도 요청을 관리하기 위해 필요했던 매핑 작업, 서블릿 등록, 컨텍스트 설정들을 구성해주고 있기에 바로 실행하여 결과를 확인할 수 있게 되는 것을 알 수 있습니다.

 

이것이 Spring과 Spring Boot 제일 큰 차이이며, Spring Boot를 사용해야하는 근본적인 이유임을 알 수 있습니다. 

 

 

참고 자료

 

9주 차 시작!

 

자바에서 예외 처리 방법 (try, catch, throw, throws, finally)

try Statements

try {
        // Do SomeThing..
}

예외가 발생할 수 있는 로직을 실행할 때 예외가 발생하는 것을 잡아내기 위해 사용하는 Statements이다. 주로 JDBC Connection이나 File Reader, API에 대한 URL Connection 등을 처리하는 로직을 감싸는 데 사용한다.

 

Ex) JDBC

try {
        connection = dataSource.getConnection(); 

        //SQLException 이 발생 가능하다.
    ResultSet resultSet = connection.prepareStatement("SELECT * FROM USER")
                        .executeQuery();
} // catch Statements

 

catch Statements

try {
        // Do SomeThing
} catch (Exception exception) {
        // Stack Trace, Ignore 등
}

Try Statements의 내부에서 발생한 Exception을 잡아 사전 정의된 동작을 하는 Statements 이다.

 

Ex) JDBC

try {
        connection = dataSource.getConnection(); 

    ResultSet resultSet = connection.prepareStatement("SELECT * FROM USER")
                        .executeQuery();
} catch (SQLException exception){

        // 해당 예외가 발생한 시점까지의의 메서드 정보를 호출한다.
        exception.stackTrace();
}

 

Multiple catch

Try Statements에서 발생한 2개 이상의 Exception에 대한 Catch Statements를 처리하는 것을 말한다.

 

JDK 7 이전

try {
        // Do Something..
} catch (IOException exception) {
        exception.stackTrace();
} catch (FileNotFoundException exception) {
        exception.stackTrace();
}

발생하는 Exception들에 대해 동일하게 stackTrace()를 호출하는 것을 볼 수 있다. 모든 Exception에 대해 같은 처리를 한다고 생각하면 같은 로직이 중복되는 것을 알 수 있다.

 

JDK 7 이후 버전부터는 해당 부분에 대해 개선이 이루어짐으로써

try {
        // Do Something..
} catch (IOException exception | FileNotFoundException exception) {
        exception.stackTrace();
} 

이런 식으로 처리할 수 있다.

 

throw

메서드 내에서 사용 가능하며, 로직을 진행하면서 조건에 따라 명시적으로 예외를 던질 때 사용하는 키워드이다. Check Exception은 해당 키워드를 통해 전파시킬 수 없으며 인스턴스만 전달 가능하고 한 번에 여러 예외를 전달할 수 있게끔 작성할 수 없다.

 

Spring에서는 ExceptionHanler를 이용하여 원하는 처리를 진행할 수 있다.

public void someMethod() {
        // Do Something..
        if(a < 1) {
                throw new RuntimeException();
        }
        // Do Something
}

// 당연히 Try-catch 문도 이용 가능하다.
try {
            throw new RuntimeException("hello");
} catch (RuntimeException exception){
            exception.printStackTrace();
}

 

throws

메서드 시그니쳐에 사용 가능하며, 해당 메서드를 호출하는 클라이언트에게 예외를 던진다.

해당 키워드는 Check Exception도 전달할 수 있으며, 메서드 뒤에 예외 클래스 형태로 정의된다.

쉼표를 이용하여 여러 Exception을 던질 수 있음을 선언할 수 있다.

void userDaoTest_selectOnes() throws SQLException {
        ResultSet resultSet = connection.prepareStatement("SELECT * FROM USER WHERE ID = 3")
                        .executeQuery();
}

void userDaoTest_selectOnes() throws IOException, FileNotFoundException{
        // Do Someting..
}

 

finally

try문, try-catch 문의 과정, 결과와 상관없이 마지막에 꼭 실행하여야 하는 로직을 정의하는 문법, 주로 사용된 Resource에 대해 반납하는 코드를 작성한다.

} finally {            
        // 리소스 반납 코드
        try {
                if (ps != null) {
                        ps.close();
                }
                if (con != null) {
                        con.close();
                }
        } catch (SQLException exception) {
                exception.printStackTrace();
        }
}

 

try-catch-finally

위에서 언급했던 try, catch, finally 문법들을 같이 사용하는 것을 의미한다.

Connection con = null;
PreparedStatement ps = null;

try {
        con = dataSource.getConnection();
        ps = con.prepareStatement("SQL");
        ps.executeUpdate();

} catch (SQLException exception) {
        exception.printStackTrace();

} finally {            
        // 리소스 반납 코드
        try {
                if (ps != null) {
                        ps.close();
                }
                if (con != null) {
                    con.close();
                }
        } catch (SQLException exception) {
                exception.printStackTrace();
        }
}

해당 로직의 경우 하는 일이 많이 없기에 가독성의 대한 큰 문제를 느끼지 못하지만, 복잡한 흐름을 가지기에 로직이 복잡해질수록 가독성이 떨어지게 된다. 추가적으로 반환되어야 할 자원이 많을수록 개발자가 어떠한 자원에 대하여 정리하는 로직을 작성하지 못해 문제를 일으킬 수 있다.

 

이를 해결하기 위해 JDK 7부터는 AutoCloseable 인터페이스를 구현한 클래스들에 대해 자동적으로 자원을 회수할 수 있는 문법을 제공한다. 이를 try-with-resources 문법이라고 한다.

 

try-with-resources

해당 방식은 여러 로직을 세미콜론으로 구분하여 선언하게 된다. 해당 방식은 로직이 진행되고, 내부적으로 close() 호출하여 모든 리소스를 반납하게 된다.

try (Connection con = dataSource.getConnection();
         PreparedStatement ps = con.prepareStatement("SELECT * FROM USER")){

        ResultSet resultSet = ps1.executeQuery();

} catch (SQLException exception) {
        exception.printStackTrace();
}

 

자바가 제공하는 예외 계층 구조

 

출처 : https://coderanch.com/t/627585/certification/Error-unchecked-exception

 

Throwable

Java에 존재하는 모든 예외와 오류에 대한 상위 클래스이다. 해당 클래스를 상속 받음으로써 thorw 문이나 catch 문 등에 사용할 수 있는 파라미터가 될 수 있으며, 추가적으로 JVM에 의해 throw 될 수 있다.

 

주로 사용되는 getLocalizedMessage, printStackTrace, initCause, toString 등의 메서드들을 구현한다.

 

Exception

프로그램 실행 중 개발자가 구현한 로직에서 던져진 문제에 대한 정보를 담는 클래스를 말하며, 자바에서의 Exception은 컴파일 시점에서 컴파일러에게 발견되는 확인된 예외이다.

 

Exception와 해당 클래스를 상속받는 Sub Class들(Runtime Exception 제외)은 try, catch Statements로 감싸거나 throw를 작성하는 등 명시적인 예외 처리를 하여야 한다.

 

Error

프로그램 실행 중 시스템에서 발생한 문제에 대한 정보를 담는 클래스를 말하며, JVM에 의해 던져지는 것이다. Application 로직에서 잡을 수 없는 대처 불가능한 것이기에 프로그램 로직을 구현할 때 상정하지 않아도 된다.

(심각한 수준의 상황을 의미한다.)

 

Exception과 Error의 차이는 JVM, WAS와 같은 System Level의 문제와 Application Level의 문제의 차이이다.

 

RuntimeException

Exception을 상속받는 클래스이며, 컴파일 시점에서 식별되지 않는 (Unchecked) Exception들의 집합이다. 주로 개발자의 실수로 인해 발생하는 예외이다. (Array Index를 넘어간다던지, 0으로 나누는 등)

 

RuntimeException과 RE가 아닌 것의 차이는?

 

Exception과 (Runtime Exception을 제외한) Sub Class들 (Checked Exceptions)

  • 컴파일 시점에서 compiler에서 확인할 수 있는 예외이다.
  • 프로그램 실행, 구현의 흐름상 예외의 발생 가능성이 있다면 명시적인 예외처리를 작성하게 강제한다.
  • IOException, ServletException, SQLException 등이 있다.

 

RuntimeException과 Sub Class들 그리고 Error (UnChecked Exceptions)

  • 런타임 시점에서 발생하는 Exception, Error들을 말한다. Checked Exception보다 좀 더 구체적인 뜻을 가지고 있다.
  • 개발자 부주의로 인한 문제에서 발생하게끔 의도된 것들이거나(null 체크, 형 변환, 메서드 호출) 시스템에서 발생하는 것들을 말한다.

NullPointerException, IllegalArgumentException, SecurityException. IndexOutOfBoundsException

ClassCastException 등이 있다.

 

커스텀한 예외 만드는 방법

Custom Exception을 만들 때에는 해당 예외의 특징을 고려하여 extend를 통해 Exception이나 RuntimeException을 상속받아 작성한다.

 

개인적으로 해당 Exception에 대해 좀 더 명확한 정보 전달을 위해 유사한 표준 에러를 상속받아 좀 더 구체적인 Custom Exception 들을 작성하고 있다.

// Custom Runtime Exception
public class NoSuchPostException extends NoSuchElementException {

    // Do SomeThing..

    public NoSuchPostException(String errorMessage) {
        super(errorMessage);
    }

}

 

커스텀 예외를 사용한다는 것은 표준 예외에 비해 좀더 명확한 정보를 전달할 수 있음을 의미하지만, 오용하게 될 경우 지나치게 많은 클래스가 만들어짐으로써 메모리의 문제( Metaspace는 상관없으며, Perm gen을 사용하는 경우)와 클래스 로딩 문제가 발생할 수 있다.

 

그렇기에 그저 이름을 바꿔 구현하는 것보다는 범용 에러를 활용하면서 메시지를 잘 작성하고 꼭 필요한 시점에만 명확한 정보를 추가적으로 제공함으로써 디버그를 진행함에 있어서 도움이 되게끔 잘 활용하여야 한다.

'Live Study' 카테고리의 다른 글

Live Study_Week 12. Annotation  (0) 2021.02.02
Live Study_Week 11. Enum  (0) 2021.01.28
Live Study_Week 08. 인터페이스  (0) 2021.01.05
Live Study_Week 07. 패키지  (0) 2020.12.28
Live Study_Week 06. 상속  (0) 2020.12.21

Logging?

서비스 동작 시 시스템 상태, 작동 정보를 시간의 경과에 따라 기록하는 것을 말한다.

 

 

로깅을 사용하는 이유?

서비스 동작 상태를 파악하고, 발생한 장애를 알려주거나, 파악하기 위해 사용한다. 그러기 위해서는 Log Message에 Context를 담아주어야 한다.

 

어떤 위치에서 어떤 Param을 사용하였고 어떤 것이 실패하였다는 느낌으로 작성하자.

 

 

로깅을 사용할 때 주의할 점?

  • Log 파일 / DB 생명 주기 & 저장소 용량
  • 개인 정보
  • 시스템 주요 정보 (시스템 보안, 계정 정보)

 

 

Logging을 사용하는 방법?

  • Linux System API(sysout)
  • Java API(sysout)
  • Java API(util.Logging)
  • Logging Framework

 

 

Spring Boot에서 제공하는 Logging Framework?

  • 기본적으로 내부 로깅 동작에 대해서는 common-logging api를 사용한다.
    • 별도의 구현체인 Logback, Java API(util.Logging) 등을 활용할 수 있다.
  • boot stater의 logging 의존성을 제외함으로써 별도의 Framework를 적용 가능하다.

 

 

 

Slf4j Library?

Slf4j는 여러 Logging Framework를 하나의 통일된 방식으로 사용하게끔 지원하는 추상 API이다.

  • Spring Boot Module이 제공하는 AutoConfiguration과 의존성을 그대로 사용할 경우 실제 Binding되는 Module은 Logback이다.

 

 

Slf4j Module Structure

API Module

  • slf4j의 Logging Interface Module이다.
  • 해당 모듈은 추상화된 인터페이스만을 제공하여 Binding될 다양한 로깅 프레임워크를 실행, 배포 시에 선택적으로 주입하도록 지원한다. 이를 통해 추상화 수준을 높이고 일관적인 사용이 가능해진다.

 

Bridge Module

  • 기존에 개발된 레거시 코드를 위해 사용하는 Module이다.
  • 기존에 작성된 로거 호출을 가로채 slf4j API로 전달하고 Binding Module과 연결된 Logger를 작동시킨다.
  • 여러 개의 Bridge Module 들을 가질 수 있다.
  • Binding Library와 같이 사용해서는 안된다.

 

 

Binding Module

  • 여러가지 Logger Framework 를 일관성 있는 API Module의 Interface와 연결해주는 역할을 한다.
  • API Interface가 호출하는 Logger API는 Binding을 통해 연결된 실제 Logger Framework이다.
    • Logback, Log4j (v 1.x, 2.x) 등

 

 

이것을 사용함으로써

  • Facade Pattern을 이용하여 구현부를 숨기고 추상화된 인터페이스를 제공한다.
  • 다른 Logging 구현체를 사용하더라도 Application Code가 변경되지 않는다.
    • 이를 통해 Application 배포 시에는 원하는 Library로 변경할 수 있게끔 한다.
    • 개발 시에는 logback을 사용한다면, 최종 배포 등에는 Log4j2 를 사용하는 등

 

 

Slf4j Logging Level

Logging Level Description
FATAL Application이 종료될 정도의 심각한 에러를 의미한다.
ERROR 에러가 발생하였으나, Application이 종료될 정도는 아님을 의미한다.
WARN 에러가 될 수 있는 잠재적 가능성이 존재함을 의미한다.
INFO Application의 상태를 간단하게 확인할 때 사용한다.
DEBUG INFO Level보다 더 자세한 정보가 필요한 경우 사용한다.
TRACE DEBUG보다 더 자세한 정보를 제공하며, 개발 환경에서 버그를 해결하기 위해 사용한다.
  • FATAL
    • 에러로 인한 비정상 종료 시에 사용되는데, 해당 상황에서는 Log가 남지 않을 수 있음을 고려하여야 한다. (상대적으로 많이 사용되지는 않는다.)

 

  • ERROR
    • FATAL, ERROR Level은 의도하지 않은 Exception 정보를 파악하는 데 사용한다.
    • 외부 API 호출 시 ERROR가 반환된다거나, 시스템 내부 에러가 발생하였을 때 사용한다.

 

  • WARN
    • 인메모리 캐시, DB 커넥션 같은 Resource 가 소진되어갈 때 해당 Level을 통해 정보를 수집하여, 알림을 받아 해결하거나 에러 발생 시 이유를 파악하는 용도로 사용한다.

 

  • INFO
    • 명확한 의도를 가지는 Exception 정보를 Logging 하는 데 사용한다.
    • Application의 Service Flow가 정상 동작하는지 확인하는 수준의 간단한 Level이다.

 

  • DEBUG
    • 권한이 없어 디버깅을 진행할 수 없을 경우, 필요한 level이다.

 

 

 

Logback?

log4j의 여러 문제점을 개선한 Logging Framework이며, 개념적으로 매우 유사하다. ( 이는 동일한 개발자가 프로젝트를 리드하였기에 그렇다고 한다. )

 

 

Logback vs Log4j 

 

Logback은 Log4j에 비하여

  • 더 빠른 구현 = Logging이 빠르게 수행되며, 메모리 공간을 상대적으로 적게 사용한다.
  • 상대적으로 견고하고 신뢰성 있는 내부 테스트들을 수행한다. (테스트 케이스 ^)
  • 다른 Logging Library 전환 시에 관련된 작업을 최대한 줄일 수 있다.
  • 구성 파일 수정 시 자동으로 해당 내용들을 적용시킨다.
  • I/O 장애로부터 원활한 복구가 가능하다.
    • Logging을 다시 시작하기 위해 Application 재구동이 필요 없다.
  • Log File의 용량, 기간 등을 설정 값을 통해 관리 가능하다.
  • Log File에 대한 비동기 방식의 자동 압축을 지원한다.
  • 구성 파일에서 조건 식을 이용한 분기 처리가 가능하다.
  • 많은 필터 기능을 제공한다.
  • Servlet Container와 통합 됨으로써 HTTP-Access Log 기능을 제공한다.

등을 추가하고 개선하였다.

 

 

Logback Module Structure

core

  • classic, access Module의 기반이 되는 Module이다.

 

classic

  • core를 확장하는 Module 로써 log4 j의 기존 기능을 개선하여 구현하였다.
  • 기본적으로 slf4j API를 이용함으로써 log4j, jul, jcl 와 같은 여러 로깅 Framework들과 쉽게 전환을 할 수 있다.

 

access

  • Tomcat, Jetty와 같은 Servlet Container와 통합되어 HTTP-Access Log 기능을 제공한다.
  • classic Module과는 독립적으로 구성되며, Container 수준에서 설치되어 사용돼야 한다.

 

 

Logback Architecture

Logger, Appender, Layout Interface들이 모여서 Logback Architecture를 구성한다.

 

 

Logger

  • Application이 Log Message를 생성하는 역할을 수행한다.
  • Logger에는 Log Level이 지정될 수 있다.
    • Logger의 Level이 지정되지 않는다면 가장 가까운 조상(상위 노드)에게 상속받는다.
    • 최상위 Root Logger는 기본 값으로 DEBUG Level이 지정되어 있다.
    • log 요청은 Level을 기준으로 같거나 상위 Level 일 때 활성화된다.
      • TRACE < DEBUG < INFO < WARN < ERROR
  • Package 단위로도 Logging Level을 지정할 수 있으며, 중복적으로 선언될 경우 최하위 Level이 적용된다.

 

Logger Context

  • 각 Logger는 로거 계층 구조에 각각의 Logger를 배치하는 logger Context와 연결된다.
  • 계층에 존재하는 모든 로거들은 LoggerFactory의 getLogger를 통해 가져올 수 있다. 
  • // 최상위 Logger // getLogger의 인자는 가져올 Logger의 이름이다. Logger rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER+NAME);
  • 이때 가져오는 Logger들은 싱글톤처럼 Application에서 하나만 존재하며 여러 Ref Variable은 같은 위치를 참조하게 된다.

Logger는 classic Module의 일부이다. 즉 해당 Module에서만 사용되는 개념이다.

 

 

Appender

  • 생성된 Log Message를 저장(출력)하는 역할을 수행한다.
  • Logger에 2개 이상의 Appender가 존재 가능하다.
    • Console Appender
    • File Appender
    • External Socket Server Appender
    • RDBMS Appender
    • JMS Appender
    • UNIX Syslog Daemon Appender
  • addAppender를 통해 주어진 Logger에 Appender를 추가할 수 있다.
    • 상위 계층 Logger에서 정의된 Appender는 하위 Logger에도 적용된다.해당 부분을 비활성화하기 위해서는 additivity flag를 false로 설정하여야 한다.
    • 이는 요청이 하위 계층에서 상위 계층으로 올라가기에 발생한다.

 

Layout

  • 출력할 Log Message 형식을 지정하는 역할을 수행한다.
  • Logback은 기본적으로 PatterLayout을 제공하며 이것을 통해 printf()와 유사한 형식을 사용할 수 있다.

Appender와 Layout은 core Module의 일부이다.

 

 

Logger Configuration

Logback의 환경 구성은 일반적으로 Application 초기화 시에 수행된다고 한다.

  • 선호되는 방식은 Configuration File을 읽는 것이다.

 

 

 

Log4j2 vs Logback

Log4j2 는 Logback 를 사용하는 것에 대해 여러 이점이 있다고 한다.

https://logging.apache.org/log4j/2.x/performance.html

  • 멀티 스레드 환경에서의 Logging 성능 차이 (최대 처리량)
    • 이는 Log4j2의 Logger가 낙관적 락을 사용하여 구현되는 반면에 Logback에서는 ArrayBlockingQueue를 사용하기 때문이다.
    • Appender 관련해 Queue에 Log Event를 추가할 때 잠금 경합이 발생하기도 한다.

 

https://logging.apache.org/log4j/2.x/performance.html

  • (동기성) 파일 Logging 지속 처리량
    • 모든 시스템에서 최대 지속 처리량은 가장 느린 구성요소에 의해 결정된다.
    • Logger에서는 Appender의 Massage Format, Disk I/O를 의미한다.

 

https://logging.apache.org/log4j/2.x/performance.html

  • 비동기 Logging 시 Parameterized Message Formating
    • 멀티 스레드 환경에서 성능 차이가 많이 벌어지지만(높지만), 매개 변수 수에 따라서 Formating 비용이 급격히 증가한다고 한다.

 

간단하게 살펴보니 Log4j2는 대부분의 상황에서, 특히 멀티 스레드 환경에서 성능의 이점이 큰 것을 알 수 있다.

 

 

 

하지만 왜 Log4j2를 Spring Boot의 기본 Logger로 사용하지 않을까?

내용 출처 : https://github.com/spring-projects/spring-boot/issues/16864

 

Log4j2 의 성능이 멀티 스레드 환경에서 우세함을 보이지만 이것이 개발자들에 있어 일반적으로 선택할만한 이유가 되지는 않는다고 생각한다.

  • 잘 추상화된 Interface와 Spring Boot 설정을 통해 쉽게 Logger 구현체를 변경 가능함으로 필요한 경우에 log4j2를 사용하면 된다.
  • 이것을 기본 Logger로 등록하게 된다면, 기존에 Logback을 사용하던 프로젝트들이 변경(마이그레이션)하는데 큰 어려움을 주게 된다. (Boot 1.X → 2.X ...)
  •  

이러한 내용들을 통해 성능만이 어떤 기술을 도입하고, 변경하는데 의미를 주지 않는다는 것을 알게 되었다. 특히나 Application에서 제일 비중 있는 DB (Query 성능 등)이 아닌 이상에는 더욱 그런 느낌을 받게 된다.

 

 

 

System.out.println() vs Logger

 

로그 출력, 저장 방식

Logger

  • Logger는 Log 내역을 별도의 파일에 저장할 수 있다.
  • Logger는 Layout 을 이용해 메시지에 대한 일관적인 형식을 지정할 수 있다.
  • Log의 유지 기간, 파일 용량 등을 설정하여 자동화된 관리를 제공한다. Framework 지원 여부
  • Log를 압축하여 관리할 수 있다. Framework 지원 여부
  • Log를 출력하는 데 있어서 기본적인 설정이 필요하고 인스턴스 사용을 해야 한다.
  • 다양한 저장 방식을 제공하고 일관적인 형식 지정이 가능하며, 자동화된 관리 방식을 제공한다.

System.out.println()

  • println()은 Log를 콘솔에만 출력할 수 있다.
  • 설정하지 않아도 메서드 하나로 로그를 출력할 수 있다.
  • 상황에 따라 필요하지 않은 log들도 Console에 모두 출력된다.
  • 각각의 Println() 마다 출력 형식을 지정하여야 한다.
  • 당시 날짜, 시간을 출력하지 않는 한 언제 발생한 로그인지 확인조차 어렵다.
  • 형식을 지정하는데 상대적으로 많은 노력이 필요하며, 저장이 되지 않는다.

 

 

로그 분류, 제어

Logger

  • Logger는 Log 내역을 별도의 파일, 서버, DB 등에 저장할 수 있다. (Appender)
    • Logging 레벨을 통해 log 정보들을 분리할 수 있다.
  • Logging 래벨 설정을 통해 필요한 log만 출력할 수 있다.
  • 로그를 세분화하여 관리할 수 있고 필요한 데이터만을 모니터링할 수 있다.

System.out.println()

  • 출력되는 메시지를 제어할 수 없다.
  • Application env (Was)의 로그와 섞여서 출력된다.
  • 로그가 남지 않으며, 제어를 할 수 없고 상황에 따라 정확한 모니터링이 불가능하다.

 

 

사용하는 리소스 량, 동기화 문제

Logger

  • Logger Framework는 일반적으로 Blocking Queue나 낙관적 락이 적용된 자료구조를 통해 Log를 저장하며, 별도의 출력이 없는 경우에 기록을 진행한다.
  • 서버의 실시간 처리량에 최대한 영향을 미치지 않는다.
  • FIFO 자료구조로 Log가 삽입되기 때문에 로그를 순차적으로 저장할 수 있다.

System.out.println()

  • println()은 매 실행시마다 스트림을 생성하고 I/O 작업(System.call)을 진행하는 Blocking 방식이다. 이는 로컬 개발 PC에서는 큰 성능 차이와 리소스 문제를 나타내지 않을 수 있지만, 실제 운영 중인 서버에서는 큰 차이가 발생하게 된다.
  • 서버의 실시간 처리량에 큰 영향을 미친다.
  • 로그가 순차적으로 저장되지 않기에 별도의 동기화 코드를 구현하여 사용하여야 한다.
  • 멀티 스레드에 안전하게 구현되지 않았다.
  • 요청 처리에 따라 로그가 소실될 수 있다.

 

 

System.out.println() vs Logger 결론

위에서 다룬 모든 경우 (간편한 사용 방식을 제외한)에서 Logger가 Println() 보다 우세함을 알 수 있다. 요즘 개발을 공부하고, 여러 자료를 찾아보면서 느끼는 SW의 중요 요소는 유지보수라는 것이다.

 

결국 Logger를 사용하는 것도 그러한 이유를 위해서라고 생각한다. 개발만이 그 SW의 전부가 아니기에, 사실상 운영하고 유지보수되는 기간이 더욱더 길 것은 자명하기 때문에 더 중요하게 생각하는 것은 아닐까?

 

 

 

 

[ 추가된 내용 ]

  • 2021-12-09일 쯤 공유되었던 Log4j 2.x 취약점은 2.0.0 에 추가된 JNDI Lookup 기능 때문이다.
    • 해당 취약점은 2.15.0 버전에서 해결하였으나 미흡한(?) 처리로 인해 DDOS 공격이 가능한 상태가 되어 다시 패치한 2.16.0 버전에서 해결 되었다.
    • 1.x 버전의 경우 JMSAppender를 사용하지 않아야 안전하다.
  • 2021-12-15일 쯤 공유되었던 Logback 1.2.9 이전 버전의 취약점은 그보다 치명적인 취약점은 아니나, JNDI Lookup과 관련된 취약점임은 동일하다.
    • 해당 취약점을 이용하기 위해선 3가지의 조건을 만족하여야 하는데 이게 가능한 수준이면 이미 서버가 털린 상태나 다름없지 않나 싶다.
      • 1. 공격자가 서버에 접속하여 Logback config 파일에 쓰기 권한을 가질 것
      • 2. 공격자가 쓰기 권한을 이용해 config 파일을 변조한(Scan = true) 다음 Application을 재기동 시킬 것 
      • 3. 1.2.9 버전 밑의 Logback을 사용하고 있을 것

 

 

출처

8주 차 시작!

 

인터페이스 정의하는 방법

 

인터페이스란?

인터페이스는 (구현한) 하위 인스턴스를 참조할 수 있는 타입이며, 해당 인스턴스가 어떠한 행위를 할 수 있는지 클라이언트에게 알려주는 일종의 계약서 역할을 한다.

클라이언트는 해당 행위를 알려주고 구현부는 해당 타입의 인스턴스에게 맡김으로써 정보 은닉을 지킬 수 있다. 이는 반대로 생각한다면 클라이언트는 구현부의 변경에 따른 여파가 없음을 의미한다.

 

 

인터페이스를 정의하는 방법?

{Access-Level-Modifier} interface {Name} {

        // JDK 7까지는 기본적으로 Static Method를 제외하고 추상 타입의 public Method 만을 
    // 선언할 수 있다.

        // 이 Method는 해당 Interface를 구현하는 Class에서 무조건 Overiding하여야 한다. 
        public String xxxMethod(String str);

        // Access-Level-Modifier 를 작성하지 않고 정의할 수 있으며, 기본 값은 Public이다.
        String xxxMethod(String str);

        // 상수 정의 
        // 인터페이스의 용도를 반하는 대표적인 안티패턴이다. 
        // 상수 필드를 정의하는 것은 해당 타입의 하위 구현체의 구현부를 노출하는 행위이다.
        // 클라이언트에게는 필요없는 정보이기도 하다.
        static final String name = "Lob!";

}

 

인터페이스 구현하는 방법

 

Interface(만) 를 instantiation 할 수 있을까?

public interface SampleInterface {

    String xxxMethod(String str);

}

----------

// error: SampleInterface is abstract; cannot be instantiated
SampleInterface sampleInterface = new SampleInterface();

Interface 만으로는 instantiation시킬 수 없으며, 해당 Interface를 Implement 한 Sub Class(Instance)를 통해서만 구현할 수 있다.

 

 

구현 방법 1 : 익명 클래스 방식

해당 방식은 일회성으로 사용되고, 재사용할 필요가 없는 경우에 이용할 수 있다.

SampleInterface sampleInterface = new SampleInterface() {
            @Override
            public String xxxMethod(String str) {
                return "Hello "+str;
            }
        };

        // lambda 방식 (JDK 8+)
        SampleInterface sampleInterfaceOfLambda = str -> "Hello "+str;

        System.out.println(sampleInterface.xxxMethod("lob"));
        System.out.println(sampleInterfaceOfLambda.xxxMethod("lob"));

 

구현 방법 2 : 인터페이스를 구현하는 방식

Interface의 Abstract Method를 Override한 뒤 생성하는 방식이다.

public class SampleInterfaceImpl implements SampleInterface {
    @Override
    public String xxxMethod(String str) {
        return "Hello "+str;
    }
}

----------

SampleInterface sampleInterfaceOfSubClass = new SampleInterfaceImpl();

System.out.println(sampleInterfaceOfSubClass.xxxMethod("lob"));

 

인터페이스 레퍼런스를 통해 구현체를 사용하는 방법

Interface도 Abstract Class나 Super Class같이 Sub Class를 참조하는 타입으로써 사용할 수 있다.

클래스 상속 관계같이 인터페이스 타입으로 Casting할 경우 인터페이스에 정의된 Method만을 참조하여 사용할 수 있다.

public class SampleInterfaceImpl2 implements SampleInterface {

    public String someMethod(String str) {
        return "Sub Class Hello "+str;
    }

    @Override
    public String xxxMethod(String str) {
        return "Hello "+str;
    }
}

SampleInterface sampleInterface1 = (SampleInterface) new SampleInterfaceImpl2();

sampleInterface1.xxxMethod("lob");

// error: cannot find symbol sampleInterface1.someMethod("lob");
sampleInterface1.someMethod("lob");

// 이렇게 Down Casting 하면 사용 가능하다.
((SampleInterfaceImpl2) sampleInterface1).someMethod("lob");

 

객체는 인터페이스를 사용해 참조하자. (JDK 5+)

이펙티브 자바에서는 적합한 Interface가 존재하는 경우 매개변수뿐만 아니라 반환 값, 변수, 필드 변수에 대한 모든 타입을 Interface로 선언하라고 한다. 이는 하위 구현체들이 모두 Interface 타입으로 참조가 가능하기에, 좀 더 유연한 코드를 작성할 수 있음을 의미한다.

// 좋은 예
// 해당 경우에는 컬랙션 변수가 Param이라고 가정하였을 때 ArrayList, LinkedList, Stack, 
// Vector 등이 전달되어도 문제가 발생하지 않는다.
List<String> list = new ArrayList<>();

// 나쁜 예
// 해당 경우에는 구체적인 클래스 타입을 적용하였기에 다른 List 구현체들이 오게될 경우
// 에러가 발생한다.
ArrayList<String> list = new ArrayList<>();

물론 Class가 특별한 기능을 제공하고, 해당 기능을 사용하여야 하는 경우에는 그러지 않아도 된다.

 

적합한 Interface가 존재하지 않는다면, Class의 계층 구조 중 필요한 기능을 만족하고 추상적인 클래스를 타입으로 사용하자.

 

인터페이스 상속

Interface는 Interface 끼리만 상속 관계를 연결할 수 있으며, Interface를 이용하여 다중 상속(구현)을 지원할 수 있다.

public interface SampleInterface {

    String xxxMethod(String str);

}

public interface SampleInterfaceForDefault {

    default String xxxxMethod(String str) {
        return "Hello "+str;
    }
}

public class SampleInterfaceImpl3 implements SampleInterface, SampleInterfaceForDefault {
    @Override
    public String xxxMethod(String str) {
        return "override Hello! "+str;
    }
}

----------

SampleInterfaceImpl3 sampleInterface2 = new SampleInterfaceImpl3();

sampleInterface2.xxxMethod("lob");
sampleInterface2.xxxxMethod("lob");

Interface들을 다중 구현하였을 경우 동일한 메서드 시그니쳐가 존재한다면, Override하여 사용하여야 한다. (그렇지 않다면 컴파일 에러가 발생한다.)

 

Class와 Interface를 같이 상속, 구현하였을 경우에는 Class에 존재하는 메서드가 우선권을 가진다.

 

인터페이스의 기본 메소드 (Default Method), 자바 8

JDK 8 이후부터는 Interface에 Static Method를 제공하는 것 말고도 Default Method라는 것이 생기게 되었다. 이는 기존에 사용되던 Interface의 문제점인 한번 배포된 Interface는 수정이 어렵다.라는 것을 해결하기 위하여 추가되었다고 생각한다.

 

Default Method가 도입된 JDK 8 이후에는 많은 Method들이 추가되었음을 알 수 있다.

 

이미 배포된 Interface 에 메서드를 추가하게 된다면, 기존에 해당 Interface를 사용하던 모든 프로젝트에서는 개발 중일 때에는 컴파일 에러, 실행 중 인 것들에 대해서는 NoSuchMethod Error가 발생하게 된다.

 

메서드를 추가한다면?

public interface SampleInterface {

    String xxxMethod(String str);

    // 해당 메서드가 추가되었다.
    void addMethod(String str);

}

----------

// error: <anonymous interfaceexample.InterfaceClient$1> is not abstract and does not override abstract method addMethod(String) in SampleInterface
SampleInterface sampleInterface = new SampleInterface() {
            @Override
            public String xxxMethod(String str) {
                return "Hello "+str;
            }
        };

// Multiple non-overriding abstract methods found in interface interfaceexample.SampleInterface
// 구현 방식 1 = lambda 방식 (JDK 8+)
SampleInterface sampleInterfaceOfLambda = str -> "Hello "+str;

----------

//Class 'SampleInterfaceImpl3' must either be declared abstract or implement abstract method 'addMethod(String)' in 'SampleInterface'
public class SampleInterfaceImpl3 implements SampleInterface, SampleInterfaceForDefault {
    @Override
    public String xxxMethod(String str) {
        return "override Hello! "+str;
    }
}

이렇게 해당 Interface를 구현하는 모든 클래스, 인터페이스에서 문제가 발생한다.

public interface SampleInterface {

    String xxxMethod(String str);

    default void addMethod(String str) {
        System.out.println("Hello "+str);
    }

}

default 키워드를 사용하여 Method를 추가한 뒤 구현하면 해당 문제가 발생하지 않는다.

 

추가적으로 default Method도 Public으로 인식되며, Static Method와 달리 재정의가 가능하다.

이는 default Method도 Instance와 같이 가시되며, 런타임 시점에서 Dispatch가 되기 때문이다.

 

인터페이스의 static 메서드, 자바 8

Interface의 Static Method도 JDK 8 이후에 제공되기 시작하였으며, default Method와 같이 구현부를 가지게 된다.

다른 점은 위에서 이야기하였던

  • Override가 불가능한 것

  • Instance가 아닌 Interface와 가시 되며. 컴파일 시점에서 Dispatch 된다는 것

  • 그리고 일반적인 Static Method와 달리 상속되지 않고 Interface Type을 직접 참조하여 호출해야 한다는 것이다.

    이는 Interface를 다중 상속을 하였을 경우 발생할 수 있는 문제를 방지하는 조치인 것 같다.

public interface SampleInterface {

    String xxxMethod(String str);

    default void addMethod(String str) {
        System.out.println("Hello "+str);
    }

    static void addStaticMethod(String str) {
        System.out.println("Static Hello "+str);
    }

}

----------

SampleInterface.addStaticMethod("lob");

SampleInterface sampleInterface3 = new SampleInterface() {
            @Override
            public String xxxMethod(String str) {
                return "null";
            }


            // Method does not override method from its superclass
            *@Override*
            // Inner classes cannot have static declarations
            static void addStaticMethod(String str) {
                System.out.println("Static Hello "+str);
            }
        };

해당 메서드도 인터페이스를 구현하지 않고도 Util Method를 이용하고 싶다면 구현하는 방식으로 사용하면 좋을 것 같다.

 

인터페이스의 private 메서드, 자바 9

private Method가 추가된 이유로는 인터페이스의 static, default Method의 로직을 공통화하고 재사용하기 위함이다. 이는 JDK 8에 발생했던 중복 코드 문제를 해결하게 되었다.

 

private Method도 Static, default Method 같이 구현부를 가져야한다는 동일한 제약을 가진다.

 

간단한 예시 코드

 

JDK 8

default void multiplyAfterAddingNumbers(long num1, long num2) {
        long val = num1 + num2;
        val = val * val;
        System.out.println("result = " + val);
}

default void multiplyAfterSubtractingNumbers(long num1, long num2) {
    long val = num1 - num2;
    val = val * val;
    System.out.println("result = " + val);
}

 

JDK 9

default void multiplyAfterAddingNumbers(long num1, long num2) {
        long val = multiplyNumbers(num1 + num2);
        System.out.println("result = " + val);
    }

    default void multiplyAfterSubtractingNumbers(long num1, long num2) {
        long val = multiplyNumbers(num1 - num2);
        System.out.println("result = " + val);
    }

    private long multiplyNumbers(long val) {
        return val * val;
    }

 

참고 자료

  • 이펙티브 자바

'Live Study' 카테고리의 다른 글

Live Study_Week 11. Enum  (0) 2021.01.28
Live Study_Week 09. 예외 처리  (0) 2021.01.11
Live Study_Week 07. 패키지  (0) 2020.12.28
Live Study_Week 06. 상속  (0) 2020.12.21
Live Study_Week 05. 클래스  (0) 2020.12.15

알게 모르게 한 번이라도 들어본 JDBC를 정리해보았습니다.

 

Java Database Connectivity?

  • Java와 여러 가지 데이터베이스 간의 연결을 위하여 제공되는 표준 인터페이스이다.
  • DB 벤더나, 써드파티 관련 라이브러리에서 JDBC를 구현한 드라이버를 제공한다.
  • DB 데이터 접근을 위해 계층 처리 모델을 제공한다. (기본 2계층 사용, 3계층 지원)

데이터베이스 벤더마다 각각의 SQL 문, 작성 방식을 사용함으로써 발생했던 문제점을 해결하였다.

  • 달랐던 메서드, 구조, 전역 변수 등의 모든 문법을 통일시켜 API로 명세한 것이다.

 

JDBC 구성 요소

Untitled (34)

 

JDBC DriverManager

  • DB의 드라이버 목록을 관리하는 클래스
  • Java Application의 연결 요청을 적절한 DB Driver와 매핑시킨다.
  • 이는 Driver가 가지고 있는 고유한 명칭 즉 ClassName을 이용하여 선택하게 된다.

 

DatabaseDriver

  • DB 서버와의 통신을 처리한다.

 

Connection

  • DB와 연결하기 위한 모든 메시지가 포함된 인터페이스
  • DB와의 통신은 연결(Connection) 객체를 통해서만 이루어진다.

 

Statement

  • 해당 인터페이스의 구현체가 SQL 문을 DB에 전달한다.
  • 일부 파생 인터페이스는 저장 프로시저를 실행하는 것 외에도 매개 변수를 허용한다.

 

PreparedStatement

  • Statement의 하위 인터페이스.
  • Statement의 실행 절차, 자원 사용 부분에 대하여 최적화를 진행하였다.

 

ResultSet

  • Statement 객체를 사용하여 SQL 쿼리를 실행한 뒤 데이터베이스에서 검색, 반환된 데이터를 저장한다. (쿼리에 대한 결과를 나타낸다.)
  • 데이터를 이동시키는 일종의 이터레이터 역할을 한다.

 

SQLException

  • 해당 클래스는 DB에서 발생하는 모든 오류를 나타낸다.
  • 각 벤더(Database)에서 제공하는 오류 코드를 Application에 그대로 전달한다.

 

JDBC의 처리 흐름

  • JDBC 클래스가 포함된 패키지를 로드하고 JDBC 드라이버를 등록하여야 한다.
  • DriverManager.getConnection() 혹은 datasource.getConnection() 를 이용하여 Connection 객체를 얻는다.
  • Statement 타입의 객체를 사용하여 쿼리를 실행한다.
  • 결과가 있는 경우 반환된 ResultSet.getXXX() 메서드를 통하여 데이터를 추출한다.
  • close() 를 통하여 사용한 객체들에 대한 자원을 풀에 반환한다.

Untitled (35)

Connection과 PreparedStatement 객체는 보통 Pool 방식으로 운영되며, 미리 정의된 제한된 수의 자원을 만들어두고 필요할 때 할당하고 반환하면 다시 풀에 넣는 방식으로 동작한다.

 

close()가 동작하지 않으면 풀에 저장된 자원은 계속해서 줄어들게 되고, 고갈됨으로써 문제를 일으키게 된다.

코드를 작성할 때에는 예외처리를 하여야 하며, 어떤 상황이 발생하더라도 자원이 반환되게 코드를 작성하여야 한다.

 

 

 

모든 상황에서 리소스를 반환하기 위한 예외처리 방법

 

try-catch-finally 방식

복잡한 흐름을 가짐으로써 가독성을 떨어트리게 된다

Connection con = null; 
PreparedStatement ps = null; 
try { 
	con = dataSource.getConnection(); 
    ps = con.prepareStatement("SQL"); 
    ps.executeUpdate(); 
} catch (SQLException exception) { 
	exception.printStackTrace(); 
} finally { 
	// 리소스 반납 코드 
   	try { 
    	if (ps != null) { 
        	ps.close(); 
        } 
        if (con != null) { 
        	con.close(); 
        } 
    } catch (SQLException exception) { 
    	exception.printStackTrace(); 
}

 

 

try-with-resources 방식

해당 방식은 내부적으로 close를 호출함으로써 자원을 반환한다.

try (Connection con = dataSource.getConnection(); PreparedStatement ps = con.prepareStatement("SELECT * FROM USER")){ 
	ResultSet resultSet = ps1.executeQuery(); 
} catch (SQLException exception) { 
	exception.printStackTrace(); 
}

 

 

JDBC Template

Spring은 JDBC의 반복되는 보일러 플레이트 코드를 템플릿, 콜백 패턴을 적용한 라이브러리인 Spring JDBC Template 을 제공한다. JDBC의 복잡했던 사용 방식을 저수준의 모듈, 메서드에 모아놓고 호출, 사용한다.

 

 

JDBC Template는

  • DB 연결, SQL 질의에 대한 사용한 자원을 내부적으로 정리하고 Pool에 반환한다.
  • JDBC SQLException (checked)을 RuntimeException으로 변환하여 제공함으로써, 유연한 대응을 할 수 있게끔 지원한다. (예외 처리 지원)
  • 다양한 SQL 질의 방식과 결과 추출을 제공한다.
    • ResultSetExtractor : 단일 객체에 대한 데이터 바인딩 제공 (Post, Member ....)
    • RowMapper : 객체 목록에 대한 데이터 바인딩 제공 (List, Map ....)
  • 멀티 스레드 환경에서 안전한 객체를 제공한다.
    • 객체의 행위에 대해 내부 상태변수 이용 없이 전달받은 것들을 사용하고 정리함을 의미한다.

 

템플릿 패턴?

객체의 행위(로직)에 대한 구조(템플릿)을 정의하고 일부 단계를 추상적으로 만들어 하위 클래스들에서 정의되게끔 한다.

 

이렇게 구현한 메서드는 사용자가 필요에 따라 재정의(선택)한 객체, 메서드를 호출하게끔하여 상호작용을 이용해 유연함을 제공한다.

  • 행위의 구조를 미리 구현한다.
  • 행위의 구조를 수정하지 않고도 하위 클래스를 통해 유연하게 변경하게끔 한다. (추상화)
  • 행위에 대한 유연한 구현 부를 제공함으로써 코드 재사용, 중복을 최소화한다.
  • 이를 통해 구현된 템플릿을 여러 지점에서 활용할 수 있다.

 

콜백 패턴?

호출된 객체(템플릿)가 호출한 객체(클라이언트)를 호출하는 흐름을 구현하는 패턴이다.

  • 객체가 다른 객체를 호출하면서 그 인자 값 중에 콜백 객체를 전달한다.
  • 해당 콜백 객체는 호출된 객체의 행위 중에 필요한 데이터가 있을 때 사용되어 호출한 객체에 정보를 요청하고 반환 데이터를 호출된 객체에 제공한다.
  • 구현된 방식에 따라서 콜백 객체가 최종 결과를 전달하게끔 할 수 있다.

 

JdbcTemplate의 구성 객체

 

JdbcTemplate

  • JDBC Template의 Core Class
  • JDBC 사용을 단순화하고 일반적인 예외를 방지하는 데 기능을 제공한다.
  • 설정된 Datasource 를 사용하여 JDBC Template 객체를 생성하게 된다.

 

PreparedStatementSetter

  • JDBC Template Class가 사용하는 Callback Interface
  • JDBC Template가 생성한 PreparedStatement의 매개 변수 값을 설정한다.

 

ResultSetExtractor

  • JDBC Template 질의 결과를 추출하는데 사용되는 Interface
  • ResultSet에서 결과를 추출하는 실제 작업을 수행한다.
  • 내부적으로 하나의 객체에 대해서 데이터를 Binding할 때 사용되는 Interface이다.

 

RowMapper

  • JDBC Template 질의 결과를 추출하는데 사용되는 Interface
  • 쿼리 질의 결과에 각각의 행들을 객체에 Mapping 하는데 사용된다.

 

그외에도

  • NamedParameterJdbcTemplate : ?를 통한 값 대입 대신 param 형식을 사용할 때 이용한다.
// 기존 방식 
String final query = "INSERT INTO PERSON(ID, NAME) VALUES(?, ?)"; 
Object[] args = new Object[] {1L, "Lob"}; 
template.update(query, args); 

// Param 방식 
String final query = "INSERT INTO PERSON(ID, NAME) VALUES(:id, :name)"; 
Map<String,Object> params = new HashMap<String,Object>(); 
params.put("id", 1L); 
params.put("name", "lob"); 
template.update(query, params);

 

  • SimpleJdbcTemplate : JdbcTemplate와 NamedParameterJdbcTemplate 에서 주로 사용되는 기능을 포함하는 인터페이스이다.
    • query, queryForXXX (Type, Collections), Update들이 포함되었다.

 

  • SimpleJdbcInsert, SimpleJdbcCall
    • 테이블 이름, 열 이름, 열값 이나 호출 프로시저, 함수명과 매개 변수만을 제공하게끔 구성되어있다.
  • INSERT 문과 프로시저 호출을 진행할 때 필요한 코드들을 단순화하기 위해 만들어진 클래스이다. 메타 데이터 처리를 방식을 제공한다.

 

  • MappingSqlQuery하위 클래스는 mapRow 메서드를 구현하여야 한다.
  • ResultSet의 각 행을 객체로 변환하기 위해 사용되는 추상 클래스이다.

 

  • SqlUpdateSetter를 통해 객체를 구성한 후에는 멀티스레드 환경에서 안전한 사용을 할 수 있다.
  • SQL 업데이트임을 나타내는 재사용 가능한 객체이며, 여러 형식의 Update 메서드를 제공한다.

 

  • StoredProcedure해당 타입의 서브클래스는 execute 메서드를 통해 저장 프로시저를 호출한다.
  • 관계형 데이터베이스의 저장 프로시저를 추상화한 클래스이다.

등이 존재한다.

 

Statement vs PreparedStatement

Statement의 Flow?

  1. 쿼리 문장의 분석
  2. 쿼리 컴파일
  3. 쿼리 실행

Statement는 매 작업시 위의 작성된 flow를 따르게 된다.

 

PreparedStatement는 이미 반환한 트랜잭션에 대해서는 (주로 조회성) 캐싱을 함으로써 이미 실행한 쿼리에 대해서는 불 필요한 Flow를 진행하지 않고 바로 값을 반환하게 된다.

 

 

Statement의 String SQL?

  • Statement 객체는 SQL 쿼리를 문자열 변수를 통하여 받게된다. Param들에 대해서도 문자열 연산을 통하여 제공받아야 되는데 이는
    • 코드의 가독성을 떨어트리고
    • SQL 인젝션에 취약한 환경을 만들며
    • 쿼리의 최적화가 되지 않는다.
String final query = "INSERT INTO PERSON(ID, NAME) VAULES("+postPerson.getId()+" ","+postPerson.getName()+")";

 

 

PreparedStatement의 String SQL?

  • PreparedStatement는 인자로 넘기는 문자열에 대하여서 parameters 기능을 제공한다.코드가 더 길어보이지만, 가독성을 생각하면 좀 더 나은 모습을 보인다.
String final query = "INSERT INTO PERSON(ID, NAME) VALUES(?, ?)"; 
PreparedStatement pr = con.preparedStatement(query); 

for (PostPerson person : PostPersons){ 
	pr.setInt(1, person.getId()); 
    pr.setString(2, person.getName()); pr.addBatch(); 
} 
preparedStatement.executeBatch();

 

 

PreparedStatement의 사전 컴파일, 일괄 실행?

 

사전 컴파일?

  • DB가 SQL을 받았을 경우 우선적으로 캐시를 확인하게끔 동작한다.
  • 캐시가 존재한다면 해당 정보를 제공하고, 캐시되지 않은 경우 쿼리를 실행하고 데이터를 캐싱한다.
  • 해당 기능은 non-SQL binary protocol 를 내부적으로 사용함으로써 DB와 JVM간의 통신 속도를 높인다. (패킷에 데이터가 적기 때문에 서버 간의 통신이 빨라진다. )

 

일괄 실행?

  • 대용량 쿼리를 한번에 전송 가능한 기능이다.
  • addBatch를 통하여 쿼리를 메모리에 적재하고, executeBatch를 통하여 한번에 전송한다. (내부적으로 ArrayList 를 활용한다.)
    • 매번 통신을 연결하고 데이터 전달을 진행하는 리소스들을 줄인다.
    • 한번에 여러 쿼리를 질의함으로써 불필요한 절차를 제거하고 빠르게 결과를 받는다.
// StatementImpl protected Query query; 
// mysql sub class ClientPreparedStatement 

public void addBatch() throws SQLException { 
	try { 
    	synchronized(this.checkClosed().getConnectionMutex()) { 
        	QueryBindings<?> queryBindings = ((PreparedQuery)this.query).getQueryBindings(); 
            queryBindings.checkAllParametersSet(); 
            this.query.addBatch(queryBindings.clone()); 
        } 
    } catch (CJException var6) { 
    	throw SQLExceptionsMapping.translateException(var6, this.getExceptionInterceptor()); 
    }     
} 

public void addBatch(String sql) throws SQLException { 
	try { 
    	synchronized(this.checkClosed().getConnectionMutex()) { 
        	this.batchHasPlainStatements = true; 
            super.addBatch(sql); 
    	} 
    } catch (CJException var6) { 
    	throw SQLExceptionsMapping.translateException(var6, this.getExceptionInterceptor()); 
    } 
} 

// AbstractQuery 
public void addBatch(Object batch) { 
	if (this.batchedArgs == null) { 
    	this.batchedArgs = new ArrayList(); 
    } 
    this.batchedArgs.add(batch); 
}

 

결론

PreparedStatement는 Statement가 가지고 있던 불필요한 절차, 리소스 사용, Non-Caching 등의 요소들을 최적화한 하위 클래스이다. DB와의 쿼리 성능이 중요한 Server든 아니든간에 유의미한 성능차이를 보이므로, PreparedStatement를 사용하는 것이 좋다.

 

참고자료

프로젝트에 작성된 통합 테스트들이 개별적으로 동작할 때는 아무 문제가 발생하지 않았었는데, Gradle Test를 통해 전체적으로 진행하니 몇몇 문제가 발생하게 되었다.

 

BUG!

JdbcSQLSyntaxErrorException: Table "POST" already exists;... [42101-200]

Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'postController' defined in file [C:\Users\serrl\Desktop\Mentoring\Somaeja\build\classes\java\main\com\somaeja\post\controller\PostController.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'postService' defined in file [C:\Users\serrl\Desktop\Mentoring\Somaeja\build\classes\java\main\com\somaeja\post\service\PostService.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'postMapper' defined in file [C:\Users\serrl\Desktop\Mentoring\Somaeja\build\classes\java\main\com\somaeja\post\mapper\PostMapper.class]: Unsatisfied dependency expressed through bean property 'sqlSessionFactory'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'sqlSessionFactory' defined in class path resource [com/somaeja/common/config/PersistenceConfig.class]: Unsatisfied dependency expressed through method 'sqlSessionFactory' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSource' defined in class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Hikari.class]: Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.boot.autoconfigure.jdbc.DataSourceInitializerInvoker': Invocation of init method failed; nested exception is org.springframework.jdbc.datasource.init.ScriptStatementFailedException: Failed to execute SQL script statement #1 of URL [file:/C:/Users/serrl/Desktop/Mentoring/Somaeja/build/resources/main/schema.sql]: CREATE TABLE POST ( POST_ID INTEGER PRIMARY KEY auto_increment, USER_ID INTEGER NOT NULL, LOCATION_ID INTEGER NOT NULL, IMAGE_ID INTEGER NOT NULL, TITLE VARCHAR(255) NOT NULL, CONTENT VARCHAR(255) NOT NULL, PRICE VARCHAR(255) NOT NULL, IS_NEGOTIABLE TINYINT(1) NOT NULL, IS_DIRECTTRADE TINYINT(1) NOT NULL, CREATEDATE VARCHAR(255) NOT NULL, MODIFYDATE VARCHAR(255) NOT NULL ); nested exception is org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "POST" already exists; SQL statement:
CREATE TABLE POST ( POST_ID INTEGER PRIMARY KEY auto_increment, USER_ID INTEGER NOT NULL, LOCATION_ID INTEGER NOT NULL, IMAGE_ID INTEGER NOT NULL, TITLE VARCHAR(255) NOT NULL, CONTENT VARCHAR(255) NOT NULL, PRICE VARCHAR(255) NOT NULL, IS_NEGOTIABLE TINYINT(1) NOT NULL, IS_DIRECTTRADE TINYINT(1) NOT NULL, CREATEDATE VARCHAR(255) NOT NULL, MODIFYDATE VARCHAR(255) NOT NULL ) [42101-200]
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:797)
    at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:227)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1356)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1203)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:556)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:324)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:897)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:879)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:551)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:758)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:750)
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:405)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:315)
    at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:120)
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:99)
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:124)
    ... 88 more

 

Table "POST" already exists; [42101-200] 에러는 무엇인가?

해당 H2 버전부터 테이블 삭제 명령의 동작 이 SQL 표준을 준수하도록 변경되었다고 한다. 즉 이전 버전에서는 테이블을 삭제할 때 제약 조건을 무시하였지만, 현재 버전부터 제약 조건에 의해 삭제가 실패하게 된다는 것이다.

 

짐작해보자면? 현재 Post Table에는 FK가 연결되어 있으며, 해당 테이블 삭제시 참조 무결성으로 인해 삭제가 되지 않고 중복되는 일이 발생하는 것 같다.

 

 

Gradle Test 시 예상 실행 흐름

해결 방법?

  1. 삭제를 시도했으나 실패하고 SQL 파일 실행 시 데이터가 존재하는 것이라면 강제 삭제를 할 수 있게끔 CASCADE가 명시된 삭제를 진행
  2. Test Class에 롤백 적용하기??

 

1번을 통해 문제를 해결하였다.

 

 

Schema.sql 최상단에 추가!

이를 통해 제대로 초기화되지 않았을 데이터들을 다음 테스트를 위한 데이터 생성 이전에 한번 더 정리한다.

DROP TABLE IF EXISTS POST CASCADE;
DROP TABLE IF EXISTS LOCATION CASCADE;

 

+ Recent posts