728x90

When mutil-module project set up, some issues comes up like this. Let's show example.

 

We have three module like Entity, Cloud Server and Wep API. Cloud Server imports Entity module and also Web API module import that. 

 

 

MonaEvCloudServer

group = 'kr.ksmartech.ev'
version = '1.0-SNAPSHOT'

dependencies {
	...
    implementation project(":MonaEvEntity")
    
    ...
}

 

MonaEvEntity - it was only imported role. 

group = 'kr.ksmartech.ev.entity'
version = 'unspecified'

 

MonaEvApi - Non-Fixed version

group = 'kr.ksmartech.ev.web.api'
version = "1.0-API-SNAPSHOT"

dependencies {
    ...
    
    implementation project(":MonaEvEntity")
  
    ...

}

 

EvCloudServer and EvEntity would not be a collision issue. but API will have a trouble when group would set "kr.ksmartech.ev.web.api".  Let's see below e.g

 

We want to implement like below, but group is different structure, so that make a collision. 

MonaEvCloudServer("kr.ksmartech.ev")          MonaEvApi ("kr.ksmartech.ev.web.api")

           |___________ MonaEvEntity__________|

 

MonaEvApi will require to point Component Scan. it was still be issue, althogh ComponentScan was set.

This issue is based on diffrent structure. Let's fix correct structure like this

 

MonaEvApi - Fixed version

 

group = 'kr.ksmartech.ev'
version = "1.0-API-SNAPSHOT"

dependencies {
    ...
    
    implementation project(":MonaEvEntity")
  
    ...

}​

MonaEvCloudServer("kr.ksmartech.ev")          MonaEvApi ("kr.ksmartech.ev")

           |___________ MonaEvEntity__________|

 

 

 

https://oingdaddy.tistory.com/254

 

Springboot 에서 @ComponentScan 설정 및 사용법

이전 Xml Config 방식에서 ComponentScan을 사용하는 방법은 다음과 같았다. applicationContext를 구성할때 이렇게 명시적으로 내가 읽어들여야하는 component들이 있는 package를 넣어줬다. 하지만 Springboot에서

oingdaddy.tistory.com

 

 

'Java' 카테고리의 다른 글

Java - Json 과 Gson 이란?  (0) 2024.09.23
[Spring Security] HTTP Basic Auth  (0) 2024.09.23
Improved Java Logging with Mapped Diagnostic Context (MDC)  (1) 2024.09.12
[Spring] ST_intersects method  (1) 2024.09.09
[Pageable] withSort  (0) 2024.08.30
728x90

1. Overview

In this tutorial, we will explore the use of Mapped Diagnostic Context (MDC) to improve the application logging.

Mapped Diagnostic Context provides a way to enrich log messages with information that could be unavailable in the scope where the logging actually occurs but that can be indeed useful to better track the execution of the program.

Further reading:

Creating a Custom Log4j2 Appender

Learn how to create a custom logging appender for Log4j2.

Java Logging with Nested Diagnostic Context (NDC)

Distinguish log messages from different sources with the Nested Diagnostic Context.

Creating a Custom Logback Appender

Learn how to implement a custom Logback appender.

2. Why Use MDC

Let’s suppose we have to write software that transfers money.

We set up a Transfer class to represent some basic information — a unique transfer id and the name of the sender:

public class Transfer {
    private String transactionId;
    private String sender;
    private Long amount;
    
    public Transfer(String transactionId, String sender, long amount) {
        this.transactionId = transactionId;
        this.sender = sender;
        this.amount = amount;
    }
    
    public String getSender() {
        return sender;
    }

    public String getTransactionId() {
        return transactionId;
    }

    public Long getAmount() {
        return amount;
    }
}
Copy

To perform the transfer, we need to use a service backed by a simple API:

public abstract class TransferService {

    public boolean transfer(long amount) {
        // connects to the remote service to actually transfer money
    }

    abstract protected void beforeTransfer(long amount);

    abstract protected void afterTransfer(long amount, boolean outcome);
}
Copy

The beforeTransfer() and afterTransfer() methods can be overridden to run custom code right before and right after the transfer completes.

We’re going to leverage beforeTransfer() and afterTransfer() to log some information about the transfer.

Let’s create the service implementation:

import org.apache.log4j.Logger;
import com.baeldung.mdc.TransferService;

public class Log4JTransferService extends TransferService {
    private Logger logger = Logger.getLogger(Log4JTransferService.class);

    @Override
    protected void beforeTransfer(long amount) {
        logger.info("Preparing to transfer " + amount + "$.");
    }

    @Override
    protected void afterTransfer(long amount, boolean outcome) {
        logger.info(
          "Has transfer of " + amount + "$ completed successfully ? " + outcome + ".");
    }
}
Copy

The main issue to note here is that when the log message is created, it is not possible to access the Transfer object — only the amount is accessible, making it impossible to log either the transaction id or the sender.

Let’s set up the usual log4j.properties file to log on the console:

log4j.appender.consoleAppender=org.apache.log4j.ConsoleAppender
log4j.appender.consoleAppender.layout=org.apache.log4j.PatternLayout
log4j.appender.consoleAppender.layout.ConversionPattern=%-4r [%t] %5p %c %x - %m%n
log4j.rootLogger = TRACE, consoleAppender
Copy

Finally, we’ll set up a small application that is able to run multiple transfers at the same time through an ExecutorService:

public class TransferDemo {

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        TransactionFactory transactionFactory = new TransactionFactory();
        for (int i = 0; i < 10; i++) {
            Transfer tx = transactionFactory.newInstance();
            Runnable task = new Log4JRunnable(tx);            
            executor.submit(task);
        }
        executor.shutdown();
    }
}Copy

Note that in order to use the ExecutorService, we need to wrap the execution of the Log4JTransferService in an adapter because executor.submit() expects a Runnable:

public class Log4JRunnable implements Runnable {
    private Transfer tx;
    
    public Log4JRunnable(Transfer tx) {
        this.tx = tx;
    }
    
    public void run() {
        log4jBusinessService.transfer(tx.getAmount());
    }
}
Copy

When we run our demo application that manages multiple transfers at the same time, we quickly see that the log is not as useful as we would like it to be.

It’s complex to track the execution of each transfer because the only useful information being logged is the amount of money transferred and the name of the thread that is running that particular transfer.

What’s more, it’s impossible to distinguish between two different transactions of the same amount run by the same thread because the related log lines look essentially the same:

...
519  [pool-1-thread-3]  INFO Log4JBusinessService 
  - Preparing to transfer 1393$.
911  [pool-1-thread-2]  INFO Log4JBusinessService 
  - Has transfer of 1065$ completed successfully ? true.
911  [pool-1-thread-2]  INFO Log4JBusinessService 
  - Preparing to transfer 1189$.
989  [pool-1-thread-1]  INFO Log4JBusinessService 
  - Has transfer of 1350$ completed successfully ? true.
989  [pool-1-thread-1]  INFO Log4JBusinessService 
  - Preparing to transfer 1178$.
1245 [pool-1-thread-3]  INFO Log4JBusinessService 
  - Has transfer of 1393$ completed successfully ? true.
1246 [pool-1-thread-3]  INFO Log4JBusinessService 
  - Preparing to transfer 1133$.
1507 [pool-1-thread-2]  INFO Log4JBusinessService 
  - Has transfer of 1189$ completed successfully ? true.
1508 [pool-1-thread-2]  INFO Log4JBusinessService 
  - Preparing to transfer 1907$.
1639 [pool-1-thread-1]  INFO Log4JBusinessService 
  - Has transfer of 1178$ completed successfully ? true.
1640 [pool-1-thread-1]  INFO Log4JBusinessService 
  - Preparing to transfer 674$.
...
Copy

Luckily, MDC can help.

3. MDC in Log4j

MDC in Log4j allows us to fill a map-like structure with pieces of information that are accessible to the appender when the log message is actually written.

The MDC structure is internally attached to the executing thread in the same way a ThreadLocal variable would be.

Here’s the high-level idea:

  1. Fill the MDC with pieces of information that we want to make available to the appender
  2. Then log a message
  3. And finally clear the MDC

The pattern of the appender should be changed in order to retrieve the variables stored in the MDC.

So, let’s change the code according to these guidelines:

import org.apache.log4j.MDC;

public class Log4JRunnable implements Runnable {
    private Transfer tx;
    private static Log4JTransferService log4jBusinessService = new Log4JTransferService();

    public Log4JRunnable(Transfer tx) {
        this.tx = tx;
    }

    public void run() {
        MDC.put("transaction.id", tx.getTransactionId());
        MDC.put("transaction.owner", tx.getSender());
        log4jBusinessService.transfer(tx.getAmount());
        MDC.clear();
    }
}
Copy

MDC.put() is used to add a key and a corresponding value in the MDC, while MDC.clear() empties the MDC.

Let’s now change the log4j.properties to print the information that we’ve just stored in the MDC.

It is enough to change the conversion pattern, using the %X{} placeholder for each entry contained in the MDC we want to be logged:

log4j.appender.consoleAppender.layout.ConversionPattern=
  %-4r [%t] %5p %c{1} %x - %m - tx.id=%X{transaction.id} tx.owner=%X{transaction.owner}%nCopy

Now if we run the application, we’ll note that each line also carries the information about the transaction being processed, making it far easier for us to track the execution of the application:

638  [pool-1-thread-2]  INFO Log4JBusinessService 
  - Has transfer of 1104$ completed successfully ? true. - tx.id=2 tx.owner=Marc
638  [pool-1-thread-2]  INFO Log4JBusinessService 
  - Preparing to transfer 1685$. - tx.id=4 tx.owner=John
666  [pool-1-thread-1]  INFO Log4JBusinessService 
  - Has transfer of 1985$ completed successfully ? true. - tx.id=1 tx.owner=Marc
666  [pool-1-thread-1]  INFO Log4JBusinessService 
  - Preparing to transfer 958$. - tx.id=5 tx.owner=Susan
739  [pool-1-thread-3]  INFO Log4JBusinessService 
  - Has transfer of 783$ completed successfully ? true. - tx.id=3 tx.owner=Samantha
739  [pool-1-thread-3]  INFO Log4JBusinessService 
  - Preparing to transfer 1024$. - tx.id=6 tx.owner=John
1259 [pool-1-thread-2]  INFO Log4JBusinessService 
  - Has transfer of 1685$ completed successfully ? false. - tx.id=4 tx.owner=John
1260 [pool-1-thread-2]  INFO Log4JBusinessService 
  - Preparing to transfer 1667$. - tx.id=7 tx.owner=Marc
Copy

4. MDC in Log4j2

The very same feature is available in Log4j2 too, so let’s see how to use it.

We’ll first set up a TransferService subclass that logs using Log4j2:

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Log4J2TransferService extends TransferService {
    private static final Logger logger = LogManager.getLogger();

    @Override
    protected void beforeTransfer(long amount) {
        logger.info("Preparing to transfer {}$.", amount);
    }

    @Override
    protected void afterTransfer(long amount, boolean outcome) {
        logger.info("Has transfer of {}$ completed successfully ? {}.", amount, outcome);
    }
}
Copy

Let’s then change the code that uses the MDC, which is actually called ThreadContext in Log4j2:

import org.apache.log4j.MDC;

public class Log4J2Runnable implements Runnable {
    private final Transaction tx;
    private Log4J2BusinessService log4j2BusinessService = new Log4J2BusinessService();

    public Log4J2Runnable(Transaction tx) {
        this.tx = tx;
    }

    public void run() {
        ThreadContext.put("transaction.id", tx.getTransactionId());
        ThreadContext.put("transaction.owner", tx.getOwner());
        log4j2BusinessService.transfer(tx.getAmount());
        ThreadContext.clearAll();
    }
}
Copy

Again, ThreadContext.put() adds an entry in the MDC and ThreadContext.clearAll() removes all the existing entries.

We still miss the log4j2.xml file to configure the logging.

As we can note, the syntax to specify which MDC entries should be logged is the same as the one used in Log4j:

<Configuration status="INFO">
    <Appenders>
        <Console name="stdout" target="SYSTEM_OUT">
            <PatternLayout
              pattern="%-4r [%t] %5p %c{1} - %m - tx.id=%X{transaction.id} tx.owner=%X{transaction.owner}%n" />
        </Console>
    </Appenders>
    <Loggers>
        <Logger name="com.baeldung.log4j2" level="TRACE" />
        <AsyncRoot level="DEBUG">
            <AppenderRef ref="stdout" />
        </AsyncRoot>
    </Loggers>
</Configuration>
Copy

Again, let’s run the application, and we’ll see the MDC information being printed in the log:

1119 [pool-1-thread-3]  INFO Log4J2BusinessService 
  - Has transfer of 1198$ completed successfully ? true. - tx.id=3 tx.owner=Samantha
1120 [pool-1-thread-3]  INFO Log4J2BusinessService 
  - Preparing to transfer 1723$. - tx.id=5 tx.owner=Samantha
1170 [pool-1-thread-2]  INFO Log4J2BusinessService 
  - Has transfer of 701$ completed successfully ? true. - tx.id=2 tx.owner=Susan
1171 [pool-1-thread-2]  INFO Log4J2BusinessService 
  - Preparing to transfer 1108$. - tx.id=6 tx.owner=Susan
1794 [pool-1-thread-1]  INFO Log4J2BusinessService 
  - Has transfer of 645$ completed successfully ? true. - tx.id=4 tx.owner=Susan
Copy

5. MDC in SLF4J/Logback

MDC is available in SLF4J too, under the condition that it is supported by the underlying logging library.

Both Logback and Log4j support MDC, as we’ve just seen, so we need nothing special to use it with a standard set up.

Let’s prepare the usual TransferService subclass, this time using the Simple Logging Facade for Java:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

final class Slf4TransferService extends TransferService {
    private static final Logger logger = LoggerFactory.getLogger(Slf4TransferService.class);

    @Override
    protected void beforeTransfer(long amount) {
        logger.info("Preparing to transfer {}$.", amount);
    }

    @Override
    protected void afterTransfer(long amount, boolean outcome) {
        logger.info("Has transfer of {}$ completed successfully ? {}.", amount, outcome);
    }
}
Copy

Let’s now use the SLF4J’s flavor of MDC.

In this case, the syntax and semantics are the same as in log4j:

import org.slf4j.MDC;

public class Slf4jRunnable implements Runnable {
    private final Transaction tx;
    
    public Slf4jRunnable(Transaction tx) {
        this.tx = tx;
    }
    
    public void run() {
        MDC.put("transaction.id", tx.getTransactionId());
        MDC.put("transaction.owner", tx.getOwner());
        new Slf4TransferService().transfer(tx.getAmount());
        MDC.clear();
    }
}
Copy

We have to provide the Logback configuration file, logback.xml:

<configuration>
    <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%-4r [%t] %5p %c{1} - %m - tx.id=%X{transaction.id} tx.owner=%X{transaction.owner}%n</pattern>
	</encoder>
    </appender>
    <root level="TRACE">
        <appender-ref ref="stdout" />
    </root>
</configuration>
Copy

Again, we’ll see that the information in the MDC is properly added to the logged messages, even though this information is not explicitly provided in the log.info() method:

1020 [pool-1-thread-3]  INFO c.b.m.s.Slf4jBusinessService 
  - Has transfer of 1869$ completed successfully ? true. - tx.id=3 tx.owner=John
1021 [pool-1-thread-3]  INFO c.b.m.s.Slf4jBusinessService 
  - Preparing to transfer 1303$. - tx.id=6 tx.owner=Samantha
1221 [pool-1-thread-1]  INFO c.b.m.s.Slf4jBusinessService 
  - Has transfer of 1498$ completed successfully ? true. - tx.id=4 tx.owner=Marc
1221 [pool-1-thread-1]  INFO c.b.m.s.Slf4jBusinessService 
  - Preparing to transfer 1528$. - tx.id=7 tx.owner=Samantha
1492 [pool-1-thread-2]  INFO c.b.m.s.Slf4jBusinessService 
  - Has transfer of 1110$ completed successfully ? true. - tx.id=5 tx.owner=Samantha
1493 [pool-1-thread-2]  INFO c.b.m.s.Slf4jBusinessService 
  - Preparing to transfer 644$. - tx.id=8 tx.owner=JohnCopy

It is worth noting that if we set up the SLF4J backend to a logging system that does not support MDC, all the related invocations will simply be skipped without side effects.

6. MDC and Thread Pools

MDC implementations typically use ThreadLocals to store the contextual information. That’s an easy and reasonable way to achieve thread-safety.

However, we should be careful using MDC with thread pools.

Let’s see how the combination of ThreadLocal-based MDCs and thread pools can be dangerous:

  1. We get a thread from the thread pool.
  2. Then we store some contextual information in MDC using MDC.put() or ThreadContext.put().
  3. We use this information in some logs, and somehow we forgot to clear the MDC context.
  4. The borrowed thread comes back to the thread pool.
  5. After a while, the application gets the same thread from the pool.
  6. Since we didn’t clean up the MDC last time, this thread still owns some data from the previous execution.

This may cause some unexpected inconsistencies between executions.

One way to prevent this is to always remember to clean up the MDC context at the end of each execution. This approach usually needs rigorous human supervision and is therefore error-prone.

Another approach is to use ThreadPoolExecutor hooks and perform necessary cleanups after each execution. 

To do that, we can extend the ThreadPoolExecutor class and override the afterExecute() hook:

 
public class MdcAwareThreadPoolExecutor extends ThreadPoolExecutor {

    public MdcAwareThreadPoolExecutor(int corePoolSize, 
      int maximumPoolSize, 
      long keepAliveTime, 
      TimeUnit unit, 
      BlockingQueue<Runnable> workQueue, 
      ThreadFactory threadFactory, 
      RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        System.out.println("Cleaning the MDC context");
        MDC.clear();
        org.apache.log4j.MDC.clear();
        ThreadContext.clearAll();
    }
}Copy

This way, the MDC cleanup would automatically happen after each normal or exceptional execution.

So, there is no need to do it manually:

@Override
public void run() {
    MDC.put("transaction.id", tx.getTransactionId());
    MDC.put("transaction.owner", tx.getSender());

    new Slf4TransferService().transfer(tx.getAmount());
}Copy

Now we can re-write the same demo with our new executor implementation:

ExecutorService executor = new MdcAwareThreadPoolExecutor(3, 3, 0, MINUTES, 
  new LinkedBlockingQueue<>(), Thread::new, new AbortPolicy());
        
TransactionFactory transactionFactory = new TransactionFactory();

for (int i = 0; i < 10; i++) {
    Transfer tx = transactionFactory.newInstance();
    Runnable task = new Slf4jRunnable(tx);

    executor.submit(task);
}

executor.shutdown();Copy

7. Conclusion

MDC has lots of applications, mainly in scenarios in which running several different threads causes interleaved log messages that would be otherwise hard to read.

And as we’ve seen, it’s supported by three of the most widely used logging frameworks in Java.

As usual, the sources are available over on GitHub.

 

https://www.baeldung.com/mdc-in-log4j-2-logback

 

728x90

Single-length Key KCV

The single-length key check value is a one-way cryptographic function of a key, used to verify that the key has been entered correctly.

The KCV is calculated by taking an input of constant D (64 Zero bits) and encrypting it with key K (64 bit). The 64 bit output is truncated to the most significant 24 bits which is reported as the keys KCV (Single-length Key Check Value KCV(K).).

Figure 1: Single-length Key Check Value KCV(K).

Double-length Key KCV

The double-length key check value is a one-way cryptographic function of a key, used to verify that the key has been correctly entered.

The KCV is calculated by taking an input of constant D (64 Zero bits) and key *K (128 bit string made up of two 64 bit values KL and KR ). Data value D is encrypted with KL as the key.  The result is decrypted with KR as the key. The result is then encrypted with KL as the key. The 64 bit output is truncated to the most significant 24 bits which is reported as the double-length keys *KCV (Double-length Key Check Value *KCV(*K)).

Figure 2: Double-length Key Check Value *KCV(*K)

'Cryptography' 카테고리의 다른 글

Configuring the HSM to Operate in FIPS Mode  (0) 2024.04.05
HOTP and TOTP  (31) 2024.03.21
The group Zp*  (31) 2024.03.11
Padding oracles and the decline of CBC-mode cipher suites  (117) 2024.03.08
CBC-bit Flipping  (56) 2024.03.08
728x90

개요

MySQL 서버를 포함한 RDBMS를 사용하다 보면, No-SQL DBMS 서버에 비해서 많은 데이터 타입들을 가지고 있다는 것을 알고 있을거에요. 하지만 RDBMS를 사용하면서 이런 다양한 데이터 타입에 대해서 정확한 용도와 특성을 모르면 RDBMS 서버가 어렵게 구현하고 있는 장점을 놓쳐 버릴 가능성이 높아요.

오늘은 많은 개발자와 DBA들이 잘 모르고 있는 MySQL 서버의 VARCHAR  TEXT 타입의 특성과 작동 방식에 대해서 좀 살펴보려고 해요.

 

VARCHAR 타입 궁금증

MySQL 서버를 사용해 본 개발자라면 누구나 한번쯤은 이런 궁금증을 가져 본 적이 있을거에요.

  • 만약 10 글자 이하로만 저장된다면 컬럼의 타입을 VARCHAR(10)으로 하거나 VARCHAR(1000)으로 해도 아무런 차이가 없는 것 아닐까? 오히려 VARCHAR(1000)으로 만들어 두면 나중에 더 큰 값을 저장해야 할 때 더 유연하게 대응할 수 있지 않을까 ?

아래와 같이 모든 컬럼을 VARCHAR(1000) 타입으로 또는 모든 컬럼을 TEXT 타입으로 생성한 테이블은 어떻게 생각하나요 ? 이런 모델링이 잘못되었다고 생각한다면 그 근거는 무엇인가요 ?

CREATE TABLE user (
  id       BIGINT NOT NULL,
  name     VARCHAR(1000),
  phone_no VARCHAR(1000),
  address  VARCHAR(1000),
  email    VARCHAR(1000),
  PRIMARY KEY(id)
);

그냥 지금까지 습관적으로 해오던 데이터 모델링 방법과는 다르기 때문에 잘못된 것일까요 ? MySQL 서버가 내부적으로 어떻게 작동하는지를 모르면, 이 질문에 명확한 답변을 하기는 어려울 수 있어요.

테이블의 컬럼이 많은 경우, 이 질문에 대해서 명확한 답변이 될만한 근거가 한가지 있어요. 간단한 테스트를 위해서 길이가 매우 긴 VARCHAR 컬럼을 가진 테이블을 한번 만들어 볼까요 ?


mysql> CREATE TABLE tb_long_varchar (id INT PRIMARY KEY, fd1 VARCHAR(1000000));
ERROR 1074 (42000): Column length too big for column 'fd1' (max = 16383); use BLOB or TEXT instead

mysql> CREATE TABLE tb_long_varchar (id INT PRIMARY KEY, fd1 VARCHAR(16383));
ERROR 1118 (42000): Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535. This includes storage overhead, check the manual. You have to change some columns to TEXT or BLOBs

mysql> CREATE TABLE tb_long_varchar (id INT PRIMARY KEY, fd VARCHAR(16382));
Query OK, 0 rows affected (0.19 sec)

mysql> ALTER TABLE tb_long_varchar ADD fd2 VARCHAR(10);
ERROR 1118 (42000): Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535. This includes storage overhead, check the manual. You have to change some columns to TEXT or BLOBs

tb_long_varchar 테이블은 하나의 VARCHAR 컬럼이 있는데, VARCHAR 타입의 최대 저장 가능 길이를 어느 정도로 하느냐에 따라서 테이블을 생성하지 못하게 되는 것을 확인할 수 있어요. 그리고 네번째 ALTER TABLE 문장의 실행 예제를 보면, 새로운 컬럼 추가가 실패한 것을 알 수 있어요. 이는 (에러 메시지에서도 잘 설명하고 있듯이) 이미 tb_long_varchar 테이블은 하나의 레코드가 저장할 수 있는 최대 길이가 65,535 바이트를 초과했기 때문에 더이상 새로운 컬럼을 추가할 수 없게 된 거에요.

이 예제를 통해서 MySQL 서버에서는 하나의 VARCHAR 컬럼이 너무 큰 길이를 사용하면, 다른 컬럼들이 사용할 수 있는 최대 공간의 크기가 영향을 받게 된다는 것을 확인했어요. 그래서 MySQL 서버에서는 레코드 사이즈 한계로 인해서, VARCHAR 타입의 최대 저장 길이 설정시에 공간을 아껴서 설정해야 해요.

이는 MySQL 서버 메뉴얼에서 이미 자세히 설명하고 있어요. 참고로 TEXT나 BLOB와 같은 LOB 컬럼은 이 제한 사항에 거의 영향을 미치지 않아요. 그래서 많은 컬럼을 가진 테이블에서는 VARCHAR 타입 대신 TEXT 타입을 사용해야 할 수도 있어요.

그런데 VARCHAR 타입의 길이 설정에 주의해야 하는 이유가 이거 하나 뿐일까요 ? 예를 들어서 추가로 새로운 컬럼이 필요치 않아서 아래와 같이 테이블을 모델링했다면, 이건 아무 문제가 없는 걸까요 ?

CREATE TABLE user (
  id       BIGINT NOT NULL,
  name     VARCHAR(4000),
  phone_no VARCHAR(4000),
  address  VARCHAR(4000),
  email    VARCHAR(4000),
  PRIMARY KEY(id)
);

TEXT 타입 궁금증

VARCHAR 타입은 저장 길이 설정에 대한 주의가 필요하다는 것을 간단히 한번 살펴보았어요. 그런데 VARCHAR 대신 TEXT 타입을 사용하면 길이 제한 문제가 싹 사라진다는 것을 쉽게 확인할 수 있어요. 그래서 아래와 같이 테이블을 만들면 VARCHAR 타입의 길이 설정에 대한 제약뿐만 아니라 저장하는 값의 길이 제한도 훨씬 크고 유연하게 테이블을 만들 수 있어요.

CREATE TABLE user (
  id       BIGINT NOT NULL,
  name     TEXT,
  phone_no TEXT,
  address  TEXT,
  email    TEXT,
  PRIMARY KEY(id)
);

여기에서 또 하나의 궁금증이 생기기 시작할거에요.

  • 문자열 저장용 컬럼을 생성할 때, VARCHAR와 TEXT 중에서 굳이 VARCHAR를 선택할 이유가 있을까 ? TEXT 타입은 저장 가능 길이도 훨씬 크고 테이블 생성할 때 굳이 길이 제한을 결정하지 않아도 되니 더 좋은 것 아닐까 ? 근데 왜 우리가 모델링하는 테이블에서 대부분 문자열 저장용 컬럼은TEXT 컬럼이 아니라 VARCHAR 컬럼이 사용될까 ?
 

VARCHAR vs TEXT

일반적인 RDBMS에서, TEXT(또는 CLOB)나 BLOB와 같은 대용량 데이터를 저장하는 컬럼 타입을 LOB(Large Object) 타입이라고 해요. 그리고 RDBMS 서는 LOB 데이터를 Off-Page 라고 하는 외부 공간에 저장해요. 일반적인 RDBMS에서와 같이, MySQL 서버도 레코드의 컬럼 데이터는 B-Tree (Clustering Index)에 저장(이를 Inline 저장이라고 함)하지만, 용량이 큰 LOB 데이터는 B-Tree 외부의 Off-Page 페이지(MySQL 서버 메뉴얼에서는 External off-page storage라고 해요)로 저장해요.

하지만 MySQL 서버는 LOB 타입의 컬럼을 항상 Off-Page로 저장하지는 않고, 길이가 길어서 저장 공간이 많이 필요한 경우에만 Off-Page로 저장해요.

예를 들어서 아래와 같이 2개의 레코드가 있을 때, 1번 레코드(id=1)의 fd 컬럼에 8,100 글자(8,100바이트)를 저장하면 Off-Page가 아닌 B-Tree(Clustering Index)에 Inline으로 저장해요. 하지만 2번 레코드(id=2)의 fd 컬럼에 8,101글자(8,101바이트)를 저장하면, MySQL 서버는 fd 컬럼을 Off-Page로 저장해요. 이는 MySQL 서버의 레코드 포맷에 따라서 조금씩 다르게 작동하는데, 이 예제는 innodb_default_row_format=DYNAMIC 설정을 기준으로 테스트해본 결과에요.

CREATE TABLE tb_lob (
  id INT PRIMARY KEY,
  fd TEXT
);

INSERT INTO tb_lob VALUES (1, REPEAT('A',8100));  -- // Inline 저장소
INSERT INTO tb_lob VALUES (2, REPEAT('A',8101));  -- // Off-Page 저장소

MySQL 서버의 레코드 크기 제한은 65,535 바이트이지만, InnoDB 스토리지 엔진의 레코드 크기 제한은 페이지(블록)의 크기에 따라서 달라지는데, 대부분 페이지 크기의 절반이 InnoDB 스토리지 엔진의 최대 레코드 크기 제한으로 작동해요.

InnoDB 스토리지 엔진은 레코드의 전체 크기가 이 제한 사항(16KB 페이지에서는 8,117 바이트)을 초과하면 길이가 긴 컬럼을 선택해서 Off-Page로 저장하게 되는데, 이 예제의 두번째 레코드(id=2)의 fd 컬럼 값이 커서 이 컬럼을 Off-Page로 저장한 것이에요.

MySQL 서버의 InnoDB row_format에 따른 Off-Page 저장 방식 차이는 MySQL 서버 메뉴얼을 참고해주세요. 페이지 크기가 64KB인 경우 InnoDB 최대 레코드 크기의 제한 사항이 예외적으로 조금 다르므로, MySQL 서버와 InnoDB 스토리지 엔진의 레코드 크기 제한에 대한 자세한 설명은 MySQL 서버 메뉴얼을 참고해주세요.

그런데 동일한 테스트를 아래와 같이 VARCHAR 타입 컬럼으로 해보면, VARCHAR 컬럼에 저장된 값이 큰 경우에도 Off-Page로 저장된다는 것이에요.

CREATE TABLE tb_varchar (
  id INT PRIMARY KEY,
  fd VARCHAR
);

INSERT INTO tb_varchar VALUES (1, REPEAT('A',8100)); -- // Inline 저장소
INSERT INTO tb_varchar VALUES (2, REPEAT('A',8101)); -- // Off-Page 저장소

VARCHAR 타입은 인덱스를 생성할 수 있는 반면 LOB 타입은 인덱스 생성을 할 수 없다는 이야기를 하는 사람도 있지만, 사실은 둘다 최대 크기 길이 제한만 충족시켜 주면 인덱스를 생성 할 수 있어요.

-- // 컬럼 그대로 사용시, 인덱스 생성 불가
mysql> ALTER TABLE tb_varchar ADD INDEX ix_fd (fd);
ERROR 1071 (42000): Specified key was too long; max key length is 3072 bytes

mysql> ALTER TABLE tb_lob ADD INDEX ix_fd (fd);
ERROR 1170 (42000): BLOB/TEXT column 'fd' used in key specification without a key length

-- // 컬럼 값의 길이(프리픽스)를 지정하면, 인덱스 생성 가능
mysql> ALTER TABLE tb_varchar ADD INDEX ix_fd ( fd(50) );
mysql> ALTER TABLE tb_lob ADD INDEX ix_fd ( fd(50) );

B-Tree 인덱스뿐만 아니라 전문 검색 인덱스도 TEXT 타입과 VARCHAR 타입 컬럼 모두 동일하게 생성할 수 있어요. 보면 볼수록 TEXT와 VARCHAR의 차이가 명확해지기 보다는 오히려 모호해지고 있다는 느낌일 거에요. 도대체 TEXT 컬럼과 VARCHAR 컬럼의 차이는 무엇이며, 어떤 경우에 TEXT 타입을 사용하고 어떤 경우에 VARCHAR 타입을 사용해야 할까요 ?

 

VARCHAR와 TEXT의 메모리 활용

MySQL 서버는 스토리지 엔진과 Handler API를 이용해서 데이터를 주고 받는데, 이때 MySQL 엔진과 InnoDB 스토리지 엔진은 uchar* records[2] 메모리 포인터를 이용해서 레코드 데이터를 주고 받아요. 이때 records[2] 메모리 객체는 실제 레코드의 데이터 크기에 관계 없이 최대 크기로 메모리를 할당해둬요. VARCHAR 타입은 최대 크기가 설정되기 때문에 메모리 공간을 records[2] 버퍼에 미리 할당받아둘 수 있지만, TEXT나 BLOB와 같은 LOB 컬럼 데이터의 경우 실제 최대 크기만큼 메모리를 할당해 두면 메모리 낭비가 너무 심해지는 문제가 있어요. 그래서 records[2] 포인터가 가리키는 메모리 공간은 VARCHAR는 포함하지만 TEXT 컬럼을 위한 공간은 포함하지 않아요.

uchar* records[2] 메모리 공간은 TABLE 구조체(struct) 내에 정의되어 있으며 TABLE 구조체는 MySQL 서버 내부에 캐싱되어서 여러 컨넥션에서 공유해서 사용될 수 있도록 구현되어 있어요. 즉, records[2] 메모리 버퍼는 처음 한번 할당되면 많은 컨넥션들에 의해서 재사용될 수 있도록 설계된 것이에요.
하지만 TEXT나 BLOB과 같은 LOB 컬럼을 위한 메모리 공간은 records[2]에 미리 할됭되어 있지 않기 때문에 매번 레코드를 읽고 쓸 때마다 필요한 만큼 메모리가 할당되어야 해요.

예를 들어서 아래와 같은 테이블을 생성했다면,

CREATE TABLE tb_lob (
  id INT PRIMARY KEY,
  fd TEXT
);

CREATE TABLE tb_varchar1 (
  id INT PRIMARY KEY,
  fd VARCHAR(100)
);

CREATE TABLE tb_varchar2 (
  id INT PRIMARY KEY,
  fd VARCHAR(10000)
);

tb_lob 테이블을 위한 records[2] 버퍼 공간은 16 * 2 바이트만큼 할당되고, tb_varchar1 테이블의 records[2] 버퍼 공간으로는 408 * 2 바이트를 할당해요. 그리고 마지막 tb_varchar2 테이블을 위해서는 40008 * 2 바이트를 할당해요.

  • tb_lob 테이블은 INT 타입의 컬럼(id)을 위한 4 바이트와 TEXT 값을 위한 포인터 공간 8바이트 그리고 헤더 공간 4바이트
  • tb_varchar1 테이블은 INT 타입의 컬럼(id)을 위한 4 바이트와 VARCHAR(100)타입 컬럼을 위한 공간 400바이트 그리고 헤더 공간 4바이트
  • tb_varchar2 테이블은 INT 타입의 컬럼(id)을 위한 4 바이트와 VARCHAR(10000) 타입 컬럼을 위한 공간 40000바이트 그리고 헤더 공간 4바이트

그래서 VARCHAR 타입의 컬럼을 읽을 때는 새롭게 메모리를 할당받는 것이 아니라 TABLE 구조체의 records[2] 버퍼를 이용해요. 하지만 TEXT나 BLOB와 같은 LOB 타입의 컬럼을 읽을 때는 (미리 할당해 둔 메모리 공간이 없기 때문에) 매번 필요한 크기만큼 메모리를 할당해서 사용후 해제해야 해요. LOB 컬럼의 값을 읽기 위해서 할당 및 해제하는 메모리 공간은 Performance_schema에 의해서 측정되지 않아요 (MySQL 8.0.33 기준). 그래서 LOB용 메모리 할당 해제가 실행되는지 알 수 없어서 성능 영향도를 파악하기가 어려운 상황이에요. 한가지 더 주의해야 할 것은 VARCHAR 타입에 저장된 값의 길이가 길어서 Off-Page로 저장된 경우, MySQL 서버는 TABLE 객체의 records[2] 버퍼를 사용하지 못하고 새롭게 메모리 공간을 할당해서 사용해요. 그래서 VARCHAR 타입에 매우 큰 값이 빈번하게 저장되는 경우는 주의가 필요해요.

 

컬럼 타입 선정 규칙

MySQL 서버의 내부적인 작동에서, VARCHAR와 TEXT 타입의 큰 차이점을 살펴보았어요. 지금까지 살펴본 내용을 토대로 VARCHAR나 TEXT 타입을 선택하는 규칙을 다음과 같이 정리해 볼 수 있어요.

VARCHAR

  • 최대 길이가 (상대적으로) 크지 않은 경우
  • 테이블 데이터를 읽을 때 항상 해당 컬럼이 필요한 경우
  • DBMS 서버의 메모리가 (상대적으로) 충분한 경우

TEXT

  • 최대 길이가 (상대적으로) 큰 경우
  • 테이블에 길이가 긴 문자열 타입 컬럼이 많이 필요한 경우
  • 테이블 데이터를 읽을 때 해당 컬럼이 자주 필요치 않은 경우

상대적이라는 단어가 많이 사용된 것은 DBMS 서버의 스펙이나 데이터 모델 그리고 유입되는 트래픽에 따라서 미치는 영향도가 다르기 때문이에요. 뿐만 아니라 DBMS 서버의 튜닝은 생산성(속도)과 효율성 사이에서 최적점(sweet-spot)을 찾는 과정이기 때문에 숫자 값 하나를 모든 판단의 기준으로 정하는 것은 불가능해요.

 

 

https://medium.com/daangn/varchar-vs-text-230a718a22a1

 

VARCHAR vs TEXT

개요

medium.com

 

+ Recent posts