서버리스 환경에서 성능향상을 위한 방법
Published: 2023-09-26 02:03:32

개요 
아래 고려사항은 복잡도가 높으므로, 반드시 시스템상에 테스트를 진행 후 병목현상이 발생할 경우 아래의 사항을 통해 성능을 향상시킵니다.

**캐싱**
캐싱은 이전에 처리한 결과를 저장하고 재사용함으로써 서버 부하를 줄이는 방법입니다. 이것은 자주 요청되는 데이터나 연산 결과를 메모리나 분산 캐시에 저장하여 다시 계산하지 않고 반환함으로써 성능을 향상시킵니다.

- 예제: AWS Lambda 함수 내에서 Redis를 사용하여 데이터 캐싱
- Redisson 라이브러리를 사용하여 AWS Lambda 함수 내에서 Redis를 활용한 데이터 캐싱을 구현할 수 있습니다.

```java
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import redis.clients.jedis.Jedis;

public class LambdaHandler {
    private final RedissonClient redissonClient;

    public LambdaHandler() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://your-redis-host:6379");
        redissonClient = Redisson.create(config);
    }

    public String handleRequest(Object input) {
        // Redis에서 데이터 가져오기
        String cachedData = redissonClient.getBucket("cached_data").get();
        
        if (cachedData != null) {
            return cachedData;
        } else {
            // 데이터를 계산하고 Redis에 저장
            String result = calculateData();
            redissonClient.getBucket("cached_data").set(result);
            return result;
        }
    }

    private String calculateData() {
        // 데이터 계산 로직
        return "Computed data";
    }
}
```

**데이터 압축**

- Brotli: 효율적인 데이터 압축 알고리즘으로, 데이터를 압축하고 전송할 때 대역폭을 절약합니다. 클라이언트와 서버 간 데이터 트래픽을 줄이는 데 도움을 줍니다.

- 예제
```java
import org.brotli.dec.BrotliInputStream;
import org.brotli.enc.BrotliOutputStream;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class CompressionExample {
    public static byte[] compressData(String data) throws IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        try (BrotliOutputStream brotliOutputStream = new BrotliOutputStream(outputStream)) {
            brotliOutputStream.write(data.getBytes());
        }
        return outputStream.toByteArray();
    }

    public static String decompressData(byte[] compressedData) throws IOException {
        ByteArrayInputStream inputStream = new ByteArrayInputStream(compressedData);
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        try (BrotliInputStream brotliInputStream = new BrotliInputStream(inputStream)) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = brotliInputStream.read(buffer)) > 0) {
                outputStream.write(buffer, 0, len);
            }
        }
        return new String(outputStream.toByteArray());
    }
}
```

**페이징 처리**
대량의 데이터를 처리할 때 모든 데이터를 한 번에 가져오지 말고 필요한 만큼 나눠서 가져오는 것을 의미합니다. 이렇게 하면 메모리 사용을 최적화하고 응답 시간을 개선할 수 있습니다.

- 자바와 Spring Framework를 사용하여 데이터를 페이징 처리하는 예제:
- 예제
```java
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.util.List;

@Entity
public class Item {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    // 다른 필드들...

    // Getter와 Setter 메서드
}

@Repository
public interface ItemRepository extends JpaRepository<Item, Long> {
    // 페이징 처리된 결과 반환
    Page<Item> findAll(PageRequest pageRequest);
}

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ItemController {
    @Autowired
    private ItemRepository itemRepository;

    @GetMapping("/items")
    public Page<Item> getItems(@RequestParam int page, @RequestParam int size) {
        // 페이지와 크기에 따라 데이터 가져오기
        PageRequest pageRequest = PageRequest.of(page, size);
        return itemRepository.findAll(pageRequest);
    }
}
```

**커넥션 풀**
- RDB Proxy AWS 솔루션 및 Lambda: RDB 프록시를 사용하여 데이터베이스 연결 관리를 최적화하고 Lambda 함수에서 재사용할 수 있는 커넥션 풀을 설정합니다. 이를 통해 서버리스 환경에서 데이터베이스 연결 오버헤드를 줄일 수 있습니다.

- Azure SQL 데이터베이스 솔루션: Azure SQL 데이터베이스는 자체 커넥션 풀 기능을 제공하므로 성능을 향상시키려면 이 기능을 활용하세요.

- 예제: Azure SQL 데이터베이스를 사용하여 커넥션 풀을 설정하는 예제입니다.
```java
import com.microsoft.azure.sqldatabase.ConnectionPoolDataSource;
import com.microsoft.azure.sqldatabase.SQLServerConnectionPoolDataSource;

public class AzureSqlConnectionPoolExample {
    public static void main(String[] args) {
        try {
            // Azure SQL 데이터베이스 연결 설정
            SQLServerConnectionPoolDataSource ds = new SQLServerConnectionPoolDataSource();
            ds.setServerName("your-server-name.database.windows.net");
            ds.setDatabaseName("your-database-name");
            ds.setUser("your-username");
            ds.setPassword("your-password");

            // 커넥션을 가져와서 쿼리 실행
            try (Connection connection = ds.getConnection()) {
                // 쿼리 실행 및 결과 처리
                // ...
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
```

**JSON 직렬화**
JSON 직렬화 데이터 형식을 변환하는 작업입니다. 불필요한 변환 작업을 최소화하고 효율적인 직렬화 라이브러리를 선택하여 성능을 향상시킬 수 있습니다.

- 예제
```java
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class JsonSerializationExample {
    public static void main(String[] args) throws JsonProcessingException {
        // ObjectMapper 생성
        ObjectMapper objectMapper = new ObjectMapper();

        // 객체 생성
        Person person = new Person("John", 30);

        try {
            // 객체를 JSON 문자열로 직렬화
            String json = objectMapper.writeValueAsString(person);
            System.out.println("Serialized JSON: " + json);

            // JSON 문자열을 객체로 역직렬화
            Person deserializedPerson = objectMapper.readValue(json, Person.class);
            System.out.println("Deserialized Person: " + deserializedPerson.getName() + ", " + deserializedPerson.getAge());
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
    }
}

import com.fasterxml.jackson.annotation.JsonProperty;

public class Person {
    @JsonProperty("name")
    private String name;
    
    @JsonProperty("age")
    private int age;

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}
```

**비동기 로깅**
로그 작성을 비동기로 처리하면 메인 서비스 실행에 영향을 덜 주면서 로깅을 수행할 수 있습니다. 이로써 응답 시간을 개선하고 성능을 높일 수 있습니다.

- 예제 : Log4j 2 설정 파일에는 Async 앱렌더와 AsyncFile 앱렌더가 추가로그 메시지가 비동기적으로 파일로 기록됩니다.
```xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO">
    <Appenders>
        <Console name="ConsoleAppender" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        </Console>
        <Async name="AsyncFile">
            <AppenderRef ref="FileAppender"/>
        </Async>
        <File name="FileAppender" fileName="logs/app.log">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        </File>
    </Appenders>
    <Loggers>
        <Root level="info">
            <AppenderRef ref="ConsoleAppender"/>
            <AppenderRef ref="AsyncFile"/>
        </Root>
    </Loggers>
</Configuration>
```
```java
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class AsyncLoggingExample {
    private static final Logger logger = LogManager.getLogger(AsyncLoggingExample.class);

    public static void main(String[] args) {
        // 로그 작성
        logger.info("This is an async log message.");

        // 애플리케이션 실행 코드
        // ...
    }
}
```

**N+1 회피**
데이터베이스 쿼리에서 발생하는 N+1 문제를 피하기 위해 쿼리를 최적화하고 필요한 데이터를 한 번의 쿼리로 가져오도록 노력합니다. 이렇게 하면 데이터베이스 부하를 줄이고 성능을 향상시킬 수 있습니다.

- N+1 문제를 회피하기 위한 JPA와 Hibernate를 사용한 예제:

```java
import javax.persistence.*;
import java.util.List;

@Entity
public class ParentEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
    private List<ChildEntity> children;

    // Getter 및 Setter 메서드
}

@Entity
public class ChildEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String childName;

    @ManyToOne(fetch = FetchType.LAZY)
    private ParentEntity parent;

    // Getter 및 Setter 메서드
}

public class NPlusOneExample {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("YourPersistenceUnit");
        EntityManager em = emf.createEntityManager();

        // 부모 엔티티와 자식 엔티티를 함께 조회
        List<ParentEntity> parents = em.createQuery("SELECT p FROM ParentEntity p JOIN FETCH p.children", ParentEntity.class)
                .getResultList();

        for (ParentEntity parent : parents) {
            // 부모와 자식 데이터 사용
            System.out.println("Parent: " + parent.getName());
            for (ChildEntity child : parent.getChildren()) {
                System.out.println("Child: " + child.getChildName());
            }
        }

        em.close();
        emf.close();
    }
}
```


이러한 방법들을 조합하여 서버리스 애플리케이션의 성능을 최적화가 가능합니다.