웹 서비스와의 HTTP 통신 마스터하기

  • 23 minutes to read

HTTP 및 웹 서비스의 핵심 개념

복잡한 애플리케이션을 구축하면서 애플리케이션 도메인 외부의 데이터와 서비스를 활용할 필요성이 증가합니다. 이번 강좌에서는 ASP.NET Core가 외부 웹 서비스와 연동하기 위해 제공하는 HTTP 통신 기능을 깊이 있게 살펴봅니다. 이 강좌는 HTTP 요청에 대한 기본적인 이해를 가정하고 진행됩니다.

HTTP 요청은 웹 서버나 서비스에서 데이터를 검색하는 데 활용됩니다. 요청은 주로 URL, HTTP 메서드(동사) 유형 (예: GET 또는 POST) 및 요청에 대한 메타데이터를 정의하는 헤더로 구성됩니다. 또한 요청 본문에는 서비스에 전송하려는 원시 데이터가 포함될 수 있습니다. 요청 처리 후, 서버는 상태 코드 (예: 200 OK 또는 500 Internal Error)를 포함한 응답을 반환합니다. 응답에는 메타데이터 헤더와 필요한 데이터를 포함하는 본문 (예: HTML 또는 JSON)이 함께 포함됩니다.

지금까지 애플리케이션에서 관리하는 핵심 데이터는 전용 로컬 데이터베이스에 저장되어 있었습니다. 상용 애플리케이션은 대개 로컬 데이터베이스에 직접 접근하거나 로컬 네트워크 연결을 활용하지만, 이는 항상 그렇지는 않습니다. 결국 애플리케이션 외부의 데이터와 서비스를 활용하는 기능을 구축하게 됩니다. 예를 들어, 외부 CRM 웹 서비스에서 데이터를 검색해 앱에 표시하거나 사용자 정보를 조회할 수 있습니다. 또한 애플리케이션은 고객에게 문자 메시지나 이메일 통신을 제공하는 클라우드 서비스에 데이터를 전송할 수도 있습니다. 이러한 통합을 위해 HTTP 통신 관리가 필요하며, ASP.NET Core는 이를 효율적으로 구현할 수 있는 다양한 기능을 제공합니다.

ASP.NET Core에서의 HTTP 통신 활용

ASP.NET Core에서는 HttpClient 클래스를 사용하여 HTTP 요청을 처리합니다. 최신 버전에서는 HttpClientFactory를 사용하여 인스턴스를 관리하고 구성합니다. 이 팩터리는 의존성 주입 서비스 컬렉션과 통합하는 헬퍼 메서드를 제공하며, 가장 기본적인 옵션은 AddHttpClient를 호출하여 등록합니다. 이 방식은 인스턴스와 연결 수명을 올바르게 관리하기 때문에 효과적입니다. 다만, 더 많은 요청이나 복잡성을 처리해야 할 경우 후속 강의에서 더 나은 옵션을 소개할 예정입니다.

// Startup.cs

using System.Net.Http;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace VisualAcademy
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();

            // HttpClientFactory를 사용하여 
            // HttpClient 인스턴스를 생성하고 관리하는 서비스를 추가합니다.
            services.AddHttpClient();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseStaticFiles();
            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

// HomeController.cs

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;

namespace VisualAcademy.Controllers
{
    public class HomeController : Controller
    {
        private readonly IHttpClientFactory _clientFactory;

        public HomeController(IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        }

        public async Task<IActionResult> Index()
        {
            var client = _clientFactory.CreateClient();
            var response = await client.GetAsync("https://api.hawaso.com/data");

            if (response.IsSuccessStatusCode)
            {
                var jsonResponse = await response.Content.ReadAsStringAsync();
                var dataList = JsonConvert.DeserializeObject<List<DataModel>>(jsonResponse);
                return View(dataList);
            }
            else
            {
                return View("Error");
            }
        }
    }

    public class DataModel
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
}

이 코드 예제에서는 Startup.cs에 HttpClient를 추가하고, HomeController에서 IHttpClientFactory를 사용하여 HttpClient 인스턴스를 생성하는 방법을 보여줍니다. 이후에는 외부 API를 호출하여 데이터를 가져오고, JSON으로 변환한 후, 해당 데이터를 사용하여 Index 뷰를 반환합니다. 이 예제에서는 요청이 성공적이지 않은 경우 "Error" 뷰를 반환하도록 구성되어 있습니다. 이렇게 구성하면, 다양한 HttpClient 사용 방법을 적용할 수 있으며, 앞서 언급한 팩터리와 연결 수명 관리를 사용할 수 있습니다.

HttpClient 클래스 사용 실습 예제

이제 ASP.NET Core Web API를 사용하여 ValuesController에서 GET 및 POST 요청을 처리하는 방법을 살펴보겠습니다.

  1. 먼저 새로운 ASP.NET Core Web API 프로젝트를 만듭니다.

  2. Controllers 폴더에 ValuesController.cs 파일을 생성합니다.

  3. 다음 코드를 사용하여 GET 및 POST 요청을 처리하는 기본적인 ValuesController를 구현합니다.

using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;

namespace VisualAcademy.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private static readonly List<string> Values = new List<string> { "value1", "value2" };

        // GET api/values
        [HttpGet]
        public ActionResult<IEnumerable<string>> Get()
        {
            return Ok(Values);
        }

        // GET api/values/5
        [HttpGet("{id}")]
        public ActionResult<string> Get(int id)
        {
            if (id < 0 || id >= Values.Count)
            {
                return NotFound();
            }

            return Ok(Values[id]);
        }

        // POST api/values
        [HttpPost]
        public ActionResult Post([FromBody] string value)
        {
            Values.Add(value);
            return CreatedAtAction(nameof(Get), new { id = Values.Count - 1 }, value);
        }
    }
}

이 코드에서는 ValuesController에서 두 가지 HTTP 메서드를 처리합니다.

  • GET 요청: HttpGet 특성을 사용하여 Get()Get(int id) 메서드를 정의합니다. Get() 메서드는 모든 값을 반환하고, Get(int id) 메서드는 지정된 인덱스의 값을 반환합니다. 인덱스가 범위를 벗어나면 404 Not Found 상태 코드를 반환합니다.

  • POST 요청: HttpPost 특성을 사용하여 Post() 메서드를 정의합니다. 이 메서드는 요청 본문에서 문자열 값을 읽어 Values 목록에 추가합니다. 그런 다음 새 값의 위치를 나타내는 201 Created 상태 코드와 함께 결과를 반환합니다.

  1. 프로젝트를 실행하고 https://localhost:5001/api/valueshttps://localhost:5001/api/values/0에 대한 GET 요청을 테스트해 보세요. 또한 POST 요청을 테스트하여 새 값을 추가하고 확인할 수 있습니다.

이렇게 하면 간단한 ValuesController를 사용하여 GET 및 POST 요청을 처리하는 ASP.NET Core Web API를 구현할 수 있습니다. 이 예제를 사용하여 웹 API를 확장하고 다양한 HTTP 메서드 및 엔드포인트를 구현할 수 있습니다.

아래 코드는 HttpClient를 사용하여 앞서 작성한 ValuesController의 API를 호출하는 방법을 보여줍니다. ValuesApiController라는 새로운 컨트롤러를 만들어 해당 기능을 구현하겠습니다.

  1. Controllers 폴더에 ValuesApiController.cs 파일을 생성합니다.

  2. 다음 코드를 사용하여 ValuesApiController를 구현합니다.

using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;

namespace VisualAcademy.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesApiController : ControllerBase
    {
        private readonly IHttpClientFactory _clientFactory;

        public ValuesApiController(IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        }

        // GET api/valuesapi
        [HttpGet]
        public async Task<ActionResult<IEnumerable<string>>> Get()
        {
            var client = _clientFactory.CreateClient();
            var response = await client.GetAsync("https://localhost:5001/api/values");

            if (response.IsSuccessStatusCode)
            {
                var jsonResponse = await response.Content.ReadAsStringAsync();
                var values = JsonConvert.DeserializeObject<List<string>>(jsonResponse);
                return Ok(values);
            }
            else
            {
                return StatusCode((int)response.StatusCode);
            }
        }

        // POST api/valuesapi
        [HttpPost]
        public async Task<ActionResult> Post([FromBody] string value)
        {
            var client = _clientFactory.CreateClient();
            var response = await client.PostAsJsonAsync("https://localhost:5001/api/values", value);

            if (response.IsSuccessStatusCode)
            {
                return StatusCode((int)response.StatusCode);
            }
            else
            {
                return StatusCode((int)response.StatusCode);
            }
        }
    }
}

위 코드에서 ValuesApiControllerIHttpClientFactory를 주입받아 HttpClient 인스턴스를 생성하고, ValuesController의 API를 호출합니다.

  • GET 요청: Get() 메서드를 사용하여 https://localhost:5001/api/values 엔드포인트를 호출하고 결과를 반환합니다.
  • POST 요청: Post() 메서드를 사용하여 https://localhost:5001/api/values 엔드포인트에 값을 전달하고 결과를 반환합니다.

이제 ValuesApiController를 통해 ValuesController의 API를 호출할 수 있습니다. 프로젝트를 실행하고 https://localhost:5001/api/valuesapihttps://localhost:5001/api/valuesapi/0에 대한 GET 요청을 테스트해 보세요. 또한 POST 요청을 테스트하여 새 값을 추가하고 확인할 수 있습니다.

타입드 HttpClient의 이해

HTTP 요구 사항이 복잡해질 경우, 표준 클라이언트 객체를 사용하는 것이 번거로울 수 있습니다. 요청에 대한 구조와 강력한 패턴이 필요한 경우가 있습니다. 다행히 .NET에서는 이를 지원하기 위한 추가적인 HttpClient 옵션을 제공합니다. 앞서 언급한 바와 같이, 애플리케이션에서 HttpClientFactory를 설정하는 다양한 방법이 있습니다. 기본 사용 예제를 살펴본 후, 명명된 클라이언트, 타입드 클라이언트 및 생성된 클라이언트와 같은 옵션도 사용할 수 있습니다. 이 세 가지 옵션은 모두 기본 클라이언트 유형을 다양한 시나리오에 맞게 래핑하는 서로 다른 유형의 래퍼입니다. 그러나 이 강좌에서는 이 모든 것을 상세히 다루지 않고, 타입드 클라이언트 옵션에 집중하겠습니다. 이 접근법은 대규모 구조화된 애플리케이션 및 요청 관리에 대한 Microsoft의 권장 사항입니다.

타입드 클라이언트는 기본적으로 HttpClient에 의존하는 표준 C# 서비스 클래스입니다. 예를 들어, 주문 데이터 서비스와 같은 클래스를 사용할 수 있습니다. 생성자에 HttpClient를 주입하여 일반적인 종속성 처럼 처리합니다. 그런 다음 다른 서비스와 마찬가지로 클래스에 메서드를 정의합니다. 예를 들어 GetAll, GetById, Add 등과 같은 메서드를 추가합니다. 여러 면에서 타입드 클라이언트는 이전에 구성한 CRUD 작업을 관리하기 위해 사용한 제품 리포지토리와 매우 유사합니다. 그러나 타입드 클라이언트는 DbContext를 사용하여 데이터베이스와 통신하는 대신, HttpClient를 사용하여 외부 웹 서비스와 데이터를 주고받습니다. 이 비유를 이해하고 앞으로의 학습에 참고하면 도움이 됩니다.

앞서 언급했듯이 팩터리 클래스를 통해 HttpClient를 얻어야 합니다. 그렇다면 왜 이 서비스에 직접 클라이언트를 주입하고 있을까요? Startup.cs에 타입 클라이언트 서비스 클래스를 등록하려면 AddHttpClient 헬퍼 메서드의 변형을 사용해야 합니다. 이렇게 하면 종속성 주입 컨테이너를 관리할 수 있습니다. 또한 등록 메서드를 사용하여 타입 클라이언트에 대한 추가 구성 정보를 제공할 수 있습니다. 예를 들어 웹 서비스의 기본 URL을 설정할 수 있습니다. 이렇게 하면 이전 예제에서처럼 모든 요청에 URL을 포함할 필요가 없어집니다. 다음 예제에서는 타입드 클라이언트를 사용하는 앱을 살펴보겠습니다.

// IOrderDataService.cs

using System.Collections.Generic;
using System.Threading.Tasks;
using VisualAcademy.Models;

namespace VisualAcademy.Services
{
    public interface IOrderDataService
    {
        Task<List<Order>> GetAllOrdersAsync();
        Task<Order> GetOrderByIdAsync(int id);
    }
}

// OrderDataService.cs

using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using VisualAcademy.Models;

namespace VisualAcademy.Services
{
    public class OrderDataService : IOrderDataService
    {
        // HttpClient 인스턴스를 저장할 private readonly 필드를 선언합니다.
        private readonly HttpClient _httpClient;

        // 생성자에서 HttpClient 인스턴스를 주입받습니다.
        public OrderDataService(HttpClient httpClient)
        {
            _httpClient = httpClient;
        }

        // 외부 API에서 모든 주문 목록을 가져오는 비동기 메서드를 정의합니다.
        public async Task<List<Order>> GetAllOrdersAsync()
        {
            // HttpClient를 사용하여 JSON 형식의 모든 주문 데이터를 가져옵니다.
            return await _httpClient.GetFromJsonAsync<List<Order>>("api/orders");
        }

        // 외부 API에서 주어진 ID와 일치하는 주문을 가져오는 비동기 메서드를 정의합니다.
        public async Task<Order> GetOrderByIdAsync(int id)
        {
            // HttpClient를 사용하여 JSON 형식의 주문 데이터를 가져옵니다.
            return await _httpClient.GetFromJsonAsync<Order>($"api/orders/{id}");
        }
    }
}

// Order.cs

namespace VisualAcademy.Models
{
    public class Order
    {
        public int Id { get; set; }
        public string ProductName { get; set; }
        public decimal Price { get; set; }
    }
}

// Startup.cs (부분)

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();

    // IOrderDataService 인터페이스와 OrderDataService 구현체를 서비스 컬렉션에 등록하고,
    // HttpClient에 대한 기본 구성을 설정합니다.
    services.AddHttpClient<IOrderDataService, OrderDataService>(client =>
    {
        // HttpClient의 기본 URL을 설정합니다. 
        // 이렇게 하면 모든 요청에서 기본 URL을 사용할 수 있습니다.
        client.BaseAddress = new Uri("https://api.hawaso.com/");
    });
}

// HomeController.cs (부분)

using VisualAcademy.Services;
using VisualAcademy.Models;

public class HomeController : Controller
{
    private readonly IOrderDataService _orderDataService;

    public HomeController(IOrderDataService orderDataService)
    {
        _orderDataService = orderDataService;
    }

    public async Task<IActionResult> Index()
    {
        var orders = await _orderDataService.GetAllOrdersAsync();
        return View(orders);
    }

    public async Task<IActionResult> OrderDetails(int id)
    {
        var order = await _orderDataService.GetOrderByIdAsync(id);
        return View(order);
    }
}

이 예제에서는 타입드 클라이언트를 사용하는 방법을 보여줍니다. 먼저, IOrderDataService 인터페이스와 OrderDataService 클래스를 정의하여 외부 웹 서비스에서 주문 데이터를 가져오는 방법을 설명합니다. OrderDataService는 생성자에 HttpClient를 주입받아 사용합니다.

Startup.cs에서는 AddHttpClient를 호출하여 타입 클라이언트를 서비스 컬렉션에 등록하고, 기본 URL을 설정합니다. 이렇게 하면 이전 예제에서처럼 모든 요청에 URL을 포함할 필요가 없어집니다.

마지막으로, HomeController에서 IOrderDataService를 주입받아 사용합니다. 이를 통해 IndexOrderDetails 액션에서 외부 웹 서비스와 통신하여 주문 데이터를 가져옵니다. 이 예제는 타입드 클라이언트를 사용하여 코드를 더 간결하고 구조화된 방식으로 작성할 수 있음을 보여줍니다.

POST 요청 처리를 위한 타입드 클라이언트

앞서 설명한 타입드 클라이언트를 사용하여 POST 요청을 처리하는 방법을 살펴보겠습니다. 예를 들어, 새로운 주문을 추가하는 메서드를 OrderDataService 클래스에 구현하겠습니다.

먼저, IOrderDataService 인터페이스에 새로운 주문을 추가하는 비동기 메서드를 정의합니다.

// IOrderDataService.cs
public interface IOrderDataService
{
    // ...
    Task<Order> AddOrderAsync(Order newOrder);
}

그 다음, OrderDataService 클래스에 새로운 주문을 추가하는 비동기 메서드를 구현합니다. 이 메서드는 외부 웹 서비스에 POST 요청을 보내고, 결과를 반환합니다.

// OrderDataService.cs
public class OrderDataService : IOrderDataService
{
    // ...

    // 외부 API에 새로운 주문을 추가하는 비동기 메서드를 정의합니다.
    public async Task<Order> AddOrderAsync(Order newOrder)
    {
        // HttpClient를 사용하여 JSON 형식의 새로운 주문 데이터를 보냅니다.
        var response = await _httpClient.PostAsJsonAsync("api/orders", newOrder);

        // 응답이 성공적인지 확인하고, 결과를 반환합니다.
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<Order>();
    }
}

마지막으로, HomeController에 새로운 주문을 추가하는 액션 메서드를 구현합니다. 이 액션 메서드는 사용자가 주문을 제출하면 호출됩니다.

// HomeController.cs
public class HomeController : Controller
{
    // ...

    // 주문 폼을 표시하는 액션 메서드를 구현합니다.
    public IActionResult AddOrder()
    {
        return View();
    }

    // 사용자가 주문을 제출할 때 호출되는 액션 메서드를 구현합니다.
    [HttpPost]
    public async Task<IActionResult> AddOrder(Order newOrder)
    {
        // IOrderDataService를 사용하여 새로운 주문을 외부 웹 서비스에 추가합니다.
        var createdOrder = await _orderDataService.AddOrderAsync(newOrder);

        // 주문이 성공적으로 추가되면 주문 상세 페이지로 리다이렉트합니다.
        return RedirectToAction("OrderDetails", new { id = createdOrder.Id });
    }
}

이 예제에서는 타입드 클라이언트를 사용하여 POST 요청을 처리하는 방법을 보여줍니다. 새로운 주문을 추가하는 기능을 OrderDataService에 구현하고, HomeController에서 해당 서비스를 호출하여 주문을 추가하고 결과를 표시합니다. 이렇게 하면 코드를 간결하고 구조화된 방식으로 작성할 수 있습니다.

타입드 클라이언트를 사용하는 실습 예제

이전에 작성한 ValuesController에 GET과 POST를 호출하는 실습 예제를 작성하겠습니다. 먼저, IValuesClient 인터페이스와 ValuesClient 클래스를 만들어서 ValuesController를 호출하는 방법을 정의합니다. ValuesControllerapi/values 경로를 사용하므로, 이 경로를 호출하도록 정의합니다.

// IValuesClient.cs

using System.Collections.Generic;
using System.Threading.Tasks;

namespace VisualAcademy.Clients
{
    public interface IValuesClient
    {
        Task<List<string>> GetValuesAsync();
        Task<string> AddValueAsync(string value);
    }
}

// ValuesClient.cs

using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;

namespace VisualAcademy.Clients
{
    public class ValuesClient : IValuesClient
    {
        private readonly HttpClient _httpClient;

        public ValuesClient(HttpClient httpClient)
        {
            _httpClient = httpClient;
        }

        public async Task<List<string>> GetValuesAsync()
        {
            return await _httpClient.GetFromJsonAsync<List<string>>("api/values");
        }

        public async Task<string> AddValueAsync(string value)
        {
            var response = await _httpClient.PostAsJsonAsync("api/values", value);
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }
}

Startup.cs에 타입 클라이언트를 등록하고, 기본 URL을 설정합니다.

// Startup.cs (부분)

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();

    services.AddHttpClient<IValuesClient, ValuesClient>(client =>
    {
        client.BaseAddress = new Uri("https://localhost:5001/");
    });
}

이제 HomeControllerIValuesClient를 주입받아 사용합니다. 예제에서는 Index 액션에서 값 목록을 가져오고 AddValue 액션에서 새 값을 추가합니다.

// HomeController.cs (부분)

using VisualAcademy.Clients;

public class HomeController : Controller
{
    private readonly IValuesClient _valuesClient;

    public HomeController(IValuesClient valuesClient)
    {
        _valuesClient = valuesClient;
    }

    public async Task<IActionResult> Index()
    {
        var values = await _valuesClient.GetValuesAsync();
        return View(values);
    }

    [HttpPost]
    public async Task<IActionResult> AddValue(string newValue)
    {
        await _valuesClient.AddValueAsync(newValue);
        return RedirectToAction("Index");
    }
}

이제 이 예제를 실행하면, 앱은 ValuesController를 호출하여 값을 가져오고 새 값을 추가할 수 있습니다. 이 예제에서는 타입드 클라이언트를 사용하여 코드를 간결하고 구조화된 방식으로 작성할 수 있음을 보여줍니다.

요약

이 강의에서는 ASP.NET Core에서 HTTP 통신을 처리하는 방법을 살펴보았습니다. 주로 표준 .NET HttpClient 클래스를 사용하여 외부 엔드포인트에 대한 요청과 응답을 관리합니다. 핵심적으로 HttpClientFactory를 사용하여 HttpClient 인스턴스를 관리해야 합니다. 이 클래스는 클라이언트 인스턴스가 적절하게 생성되고 폐기되도록 보장하며, 과거 개발자들에게 골칫거리가 되었던 문제를 방지합니다.

.NET은 기본, 명명된, 타입드 및 생성된 클라이언트와 같은 다양한 클라이언트 유형을 생성하여 다양한 시나리오를 처리할 수 있습니다. 특히 타입드 클라이언트를 사용하면 외부 웹 서비스나 앱과의 HttpClient 상호작용을 래핑하는 강력한 서비스 클래스를 생성할 수 있습니다. Startup.cs 클래스에서 HttpClientFactory와 타입 클라이언트를 특수 AddHttpClient 헬퍼 메서드로 등록했습니다. 이 메서드는 HttpClient 객체가 올바르게 주입되도록 보장하며, 공유 기본 URL과 같은 특정 구성을 설정할 수 있습니다.

HttpClient는 요청과 응답 처리를 단순화하는 다양한 헬퍼 메서드를 제공합니다. 이 중 JSON 데이터 처리를 쉽게하는 몇 가지 메서드가 포함되어 외부 요청을 관리할 때 가장 일반적인 작업을 쉽게 처리할 수 있습니다.

VisualAcademy Docs의 모든 콘텐츠, 이미지, 동영상의 저작권은 박용준에게 있습니다. 저작권법에 의해 보호를 받는 저작물이므로 무단 전재와 복제를 금합니다. 사이트의 콘텐츠를 복제하여 블로그, 웹사이트 등에 게시할 수 없습니다. 단, 링크와 SNS 공유, Youtube 동영상 공유는 허용합니다. www.VisualAcademy.com
박용준 강사의 모든 동영상 강의는 데브렉에서 독점으로 제공됩니다. www.devlec.com