DEV Community

NeNoVen
NeNoVen

Posted on

서버리스 환경에서 성능향상을 위한 방법

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

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

  • 예제: AWS Lambda 함수 내에서 Redis를 사용하여 데이터 캐싱
  • Redisson 라이브러리를 사용하여 AWS Lambda 함수 내에서 Redis를 활용한 데이터 캐싱을 구현할 수 있습니다.
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";
    }
}
Enter fullscreen mode Exit fullscreen mode

데이터 압축

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

  • 예제

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());
    }
}
Enter fullscreen mode Exit fullscreen mode

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

  • 자바와 Spring Framework를 사용하여 데이터를 페이징 처리하는 예제:
  • 예제
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);
    }
}
Enter fullscreen mode Exit fullscreen mode

커넥션 풀

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

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

  • 예제: Azure SQL 데이터베이스를 사용하여 커넥션 풀을 설정하는 예제입니다.

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();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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

  • 예제
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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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

  • 예제 : Log4j 2 설정 파일에는 Async 앱렌더와 AsyncFile 앱렌더가 추가로그 메시지가 비동기적으로 파일로 기록됩니다.
<?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>
Enter fullscreen mode Exit fullscreen mode
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.");

        // 애플리케이션 실행 코드
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

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

  • N+1 문제를 회피하기 위한 JPA와 Hibernate를 사용한 예제:
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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)