이 게시글은 ai한테 질문하면서 배운 내용을 ai에게 정리하라고 해 작성된 글입니다.
현대 소프트웨어 설계에서 헥사고날 아키텍처는 유연하고 확장 가능한 시스템을 구축하는 데 매우 유용한 패턴 중 하나입니다. 이 글에서는 헥사고날 아키텍처의 기본 개념, 핵심 용어, 그리고 Golang을 이용한 카페 주문 시스템의 예시를 통해 실전에서 어떻게 적용할 수 있는지를 살펴보겠습니다.
헥사고날 아키텍처(Hexagonal Architecture)란?
헥사고날 아키텍처는 비즈니스 로직과 외부 시스템을 분리하여 애플리케이션을 설계하는 방법입니다. 흔히 "포트-어댑터 아키텍처(Ports and Adapters)"라고도 불리며, 비즈니스 로직을 외부의 의존성으로부터 독립시켜 유연성과 유지보수성을 높이는 것이 목적입니다.
이 아키텍처에서는 비즈니스 로직을 중심으로, 외부 시스템과의 상호작용을 포트(인터페이스)와 어댑터(구현체)를 통해 처리합니다. 이렇게 하면 외부 시스템(DB, API 등)을 변경해도 비즈니스 로직에는 전혀 영향을 미치지 않게 됩니다.
설명: 비즈니스 로직이 담긴 애플리케이션의 핵심 부분으로, 외부 시스템(DB, 외부 API 등)과는 독립적으로 동작합니다.
예시: OrderService, Drink와 같은 비즈니스 규칙을 처리하는 서비스.
type Drink struct {
Name string
Stock int
}
설명: 비즈니스 로직에서 외부 시스템과의 상호작용을 추상화한 인터페이스입니다. 비즈니스 로직은 포트에 정의된 기능을 사용해 외부 시스템과 통신하며, 구체적인 구현은 알지 못합니다.
예시: 외부 시스템으로부터 재고를 확인하는 StockChecker, DB에 데이터를 저장하는 DrinkRepository 같은 인터페이스.
type StockChecker interface {
CheckStock(drinkName string) (int, error)
}
설명: 포트를 구현하여 외부 시스템과의 실제 통신을 담당하는 부분입니다. 포트를 통해 비즈니스 로직이 요청하는 기능을 외부 시스템에서 수행하도록 합니다.
예시: 외부 서버에 HTTP 요청을 보내는 HTTPStockChecker, DB에 데이터를 저장하는 SQLDrinkRepository.
type SQLDrinkRepository struct {
db *sql.DB
}
func (r *SQLDrinkRepository) Save(drink Drink) error {
_, err := r.db.Exec("INSERT INTO drinks (name, stock) VALUES (?, ?)", drink.Name, drink.Stock)
return err
}
설명: 외부 시스템에서 들어오는 요청에 의해 애플리케이션이 동작하는 부분입니다. 예를 들어, 외부 API에서 요청을 받거나 데이터베이스의 트리거에 반응하는 상황이 해당됩니다.
예시: DB에 데이터를 저장하는 SQLDrinkRepository는 애플리케이션이 DB에 의해 "구동"되는 부분입니다.
설명: 애플리케이션이 외부 시스템에 요청을 보내고 그 결과를 받아오는 역할을 합니다. 즉, 애플리케이션이 외부 시스템을 "구동"하는 측면입니다.
예시: 외부 서버로 재고 확인 요청을 보내는 HTTPStockChecker는 외부 시스템을 "구동"하는 어댑터입니다.
카페 주문 시스템을 통해 헥사고날 아키텍처를 어떻게 적용할 수 있는지 살펴보겠습니다. 이 예시에서는 카페 주문 서비스가 외부 서버에서 음료 재고를 확인하고, 그 데이터를 DB에 저장하는 과정을 구현합니다.
먼저, 비즈니스 로직 부분에서 음료 주문 처리를 담당하는 OrderService와 음료 도메인을 정의합니다.
package cafe
type Drink struct {
ID int
Name string
Stock int
}
type StockChecker interface {
CheckStock(drinkName string) (int, error)
}
type DrinkRepository interface {
Save(drink Drink) error
}
type OrderService struct {
stockChecker StockChecker
drinkRepository DrinkRepository
}
func NewOrderService(stockChecker StockChecker, drinkRepository DrinkRepository) *OrderService {
return &OrderService{
stockChecker: stockChecker,
drinkRepository: drinkRepository,
}
}
func (s *OrderService) OrderDrink(drinkName string) error {
stock, err := s.stockChecker.CheckStock(drinkName)
if err != nil {
return err
}
drink := Drink{
Name: drinkName,
Stock: stock,
}
return s.drinkRepository.Save(drink)
}
외부 서버와의 통신 (Driving 어댑터)
외부 서버에서 음료 재고를 확인하는 HTTPStockChecker 어댑터를 구현합니다.
package adapter
import (
"encoding/json"
"fmt"
"net/http"
)
type HTTPStockChecker struct {
apiEndpoint string
}
func NewHTTPStockChecker(apiEndpoint string) *HTTPStockChecker {
return &HTTPStockChecker{
apiEndpoint: apiEndpoint,
}
}
func (h *HTTPStockChecker) CheckStock(drinkName string) (int, error) {
resp, err := http.Get(fmt.Sprintf("%s/checkStock?drink=%s", h.apiEndpoint, drinkName))
if err != nil {
return 0, err
}
defer resp.Body.Close()
var result struct {
Stock int `json:"stock"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return 0, err
}
return result.Stock, nil
}
DB 저장소와의 통신 (Driven 어댑터)
DB에 음료 정보를 저장하는 SQLDrinkRepository 어댑터를 구현합니다.
package adapter
import (
"database/sql"
"cafe"
)
type SQLDrinkRepository struct {
db *sql.DB
}
func NewSQLDrinkRepository(db *sql.DB) *SQLDrinkRepository {
return &SQLDrinkRepository{db: db}
}
func (r *SQLDrinkRepository) Save(drink cafe.Drink) error {
_, err := r.db.Exec("INSERT INTO drinks (name, stock) VALUES (?, ?)", drink.Name, drink.Stock)
return err
}
최종적으로, OrderService에 외부 시스템과 DB 어댑터를 연결하여 주문 처리를 실행합니다.
package main
import (
"database/sql"
"fmt"
"cafe"
"adapter"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/cafedb")
if err != nil {
fmt.Println("Failed to connect to the database:", err)
return
}
defer db.Close()
stockChecker := adapter.NewHTTPStockChecker("http://inventory.api.com")
drinkRepository := adapter.NewSQLDrinkRepository(db)
orderService := cafe.NewOrderService(stockChecker, drinkRepository)
err = orderService.OrderDrink("Americano")
if err != nil {
fmt.Println("Failed to order drink:", err)
} else {
fmt.Println("Drink ordered successfully!")
}
}
헥사고날 아키텍처는 비즈니스 로직과 외부 시스템의 의존성을 분리하여 시스템의 확장성과 유지보수성을 높여줍니다. 포트(인터페이스)를 통해 외부와의 상호작용을 추상화하고, 어댑터(구현체)를 통해 구체적인 구현을 처리하는 이 방식은 외부 시스템(DB, API 등)을 교체하거나 추가할 때 매우 유연하게 대처할 수 있는 구조를 제공합니다.
카페 주문 시스템 예시를 통해 이러한 설계가 실제로 어떻게 작동하는지를 살펴보았듯이, 헥사고날 아키텍처는 다양한 환경에서 강력한 도구가 될 수 있습니다.